mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
671 Commits
2019-03-02
...
2019-08-01
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c8e313fd5 | ||
|
|
f64ec11668 | ||
|
|
9bbccd89d3 | ||
|
|
1ae3799aee | ||
|
|
260843e5b1 | ||
|
|
e3f22e5787 | ||
|
|
2aa308efdd | ||
|
|
74c18d7861 | ||
|
|
c41cccd9a6 | ||
|
|
bba34b28b8 | ||
|
|
d8a41575c8 | ||
|
|
0521de668a | ||
|
|
12441bddab | ||
|
|
bc25c52683 | ||
|
|
eb3fb70ea1 | ||
|
|
2f90ed1f32 | ||
|
|
f3dd4b028d | ||
|
|
7dcad516bd | ||
|
|
9859f99513 | ||
|
|
51b7f2777d | ||
|
|
2f2478d2d3 | ||
|
|
a43ada82b2 | ||
|
|
5149f290d0 | ||
|
|
0dc6f08deb | ||
|
|
b1f04ed96d | ||
|
|
cd49b3c89b | ||
|
|
f894d43111 | ||
|
|
4033c0c754 | ||
|
|
786b26d23e | ||
|
|
d08d8ed22c | ||
|
|
b7b62aa3f6 | ||
|
|
39d7e3c62c | ||
|
|
81b57ecf7c | ||
|
|
572e8b52e1 | ||
|
|
9b634472c6 | ||
|
|
d8bc20b1ab | ||
|
|
d2bfd59953 | ||
|
|
3d20ae47ea | ||
|
|
85cf8d89bc | ||
|
|
50e954223a | ||
|
|
109d5d16bd | ||
|
|
1672dc5946 | ||
|
|
5769944918 | ||
|
|
9ef1211d53 | ||
|
|
f2ae04597f | ||
|
|
1327de1c82 | ||
|
|
827c4e172a | ||
|
|
c300bd17fa | ||
|
|
0187fd8eae | ||
|
|
0469f0240b | ||
|
|
4aca6c5ef8 | ||
|
|
d69aee4972 | ||
|
|
3da47318b1 | ||
|
|
ef036df2bc | ||
|
|
579f68cf11 | ||
|
|
90f6ca4635 | ||
|
|
374cac0107 | ||
|
|
4d361b1952 | ||
|
|
fcee7779b0 | ||
|
|
b4191b6225 | ||
|
|
dbee37ab34 | ||
|
|
a3ad0ab09b | ||
|
|
ed0c4c117b | ||
|
|
2432151bf8 | ||
|
|
2129bfc570 | ||
|
|
8de6cd3f44 | ||
|
|
9b9831f28b | ||
|
|
8a2cac0d0c | ||
|
|
e17b105574 | ||
|
|
67c5f6b7cb | ||
|
|
d452d070a1 | ||
|
|
a846c3245d | ||
|
|
4ffa3c1b49 | ||
|
|
b2a6682798 | ||
|
|
f3aac603f8 | ||
|
|
712cb473f7 | ||
|
|
3c68a5ca65 | ||
|
|
20670bab2f | ||
|
|
86d709ae01 | ||
|
|
0aba95cc9d | ||
|
|
de3c8373fd | ||
|
|
75ecd4e72d | ||
|
|
56555a4d99 | ||
|
|
cfad20bb33 | ||
|
|
fa226bb1b9 | ||
|
|
77333ff9f7 | ||
|
|
b9a34bee51 | ||
|
|
22ee51c12c | ||
|
|
ee8d853fcb | ||
|
|
19198ea665 | ||
|
|
bcbda4d855 | ||
|
|
79a624e696 | ||
|
|
c123ca1054 | ||
|
|
9f0f35033d | ||
|
|
3633285aaa | ||
|
|
cb16790330 | ||
|
|
67055d8b56 | ||
|
|
ca37fd8f4c | ||
|
|
46b98dab70 | ||
|
|
0568996264 | ||
|
|
7baad61746 | ||
|
|
1d1e0d74f8 | ||
|
|
d53d1c616f | ||
|
|
5b05a9bc61 | ||
|
|
2c39229b13 | ||
|
|
59b5dfddec | ||
|
|
b730ac5d5a | ||
|
|
4860d8a7df | ||
|
|
9f0cde3d69 | ||
|
|
c8917e677b | ||
|
|
6c2cc206a6 | ||
|
|
5a9f3cfc1e | ||
|
|
8f28b33342 | ||
|
|
cac97a9663 | ||
|
|
2ccb564a7b | ||
|
|
d1d0430fce | ||
|
|
be251d6b03 | ||
|
|
6cfaf920ee | ||
|
|
1657f8768c | ||
|
|
c4ab0bb867 | ||
|
|
886946cc8c | ||
|
|
ed4ddcfda8 | ||
|
|
7886cd63bd | ||
|
|
69b94719a1 | ||
|
|
b4a3f66773 | ||
|
|
ab14433151 | ||
|
|
5078f6fb5c | ||
|
|
fc6d62aefb | ||
|
|
f73bccfec8 | ||
|
|
96be1a3f62 | ||
|
|
52e96e3d2a | ||
|
|
33e2721eb2 | ||
|
|
4bc44666e5 | ||
|
|
3d8e4f96c8 | ||
|
|
94457d81b6 | ||
|
|
c212bf27db | ||
|
|
59b5ee65d4 | ||
|
|
60cedca97b | ||
|
|
1a9aa60bf7 | ||
|
|
6438a5ca1f | ||
|
|
3f303511bd | ||
|
|
fb352a8d40 | ||
|
|
ea7899f47d | ||
|
|
fb6da1de4a | ||
|
|
2651b15db1 | ||
|
|
6e7a733c3c | ||
|
|
245e27c893 | ||
|
|
793c2df7ee | ||
|
|
28de629c08 | ||
|
|
210bcaa56d | ||
|
|
d7329c1bdd | ||
|
|
a5f0761a43 | ||
|
|
dd963d6161 | ||
|
|
96c0253ee2 | ||
|
|
191a7a9386 | ||
|
|
387be4a0a6 | ||
|
|
b9c2c42bc0 | ||
|
|
fffe6ed2df | ||
|
|
c4cbe9476c | ||
|
|
0a67cc3dab | ||
|
|
726e07ed5b | ||
|
|
ebb6313eef | ||
|
|
11d8f765b2 | ||
|
|
514e57b3e9 | ||
|
|
d8fb6fb951 | ||
|
|
255f0d4b2a | ||
|
|
d30e7504c2 | ||
|
|
8d0cd356fd | ||
|
|
aff40bf00a | ||
|
|
eedf7358b4 | ||
|
|
26aebcc167 | ||
|
|
9d420c727e | ||
|
|
60fe84ad16 | ||
|
|
6a44c682ad | ||
|
|
60df44f0ca | ||
|
|
ac926f5070 | ||
|
|
6e9a4a48f7 | ||
|
|
a8894b308a | ||
|
|
7cc91e1bc5 | ||
|
|
9eb51f164c | ||
|
|
a1c00e9318 | ||
|
|
17666bc059 | ||
|
|
241d29ff7c | ||
|
|
c5039a4719 | ||
|
|
fd604048db | ||
|
|
6a77ed1e07 | ||
|
|
9e38815ec4 | ||
|
|
86c325c4ec | ||
|
|
bfcc6cf12c | ||
|
|
8ba8cf7c23 | ||
|
|
6c588a1510 | ||
|
|
651fd9c4a5 | ||
|
|
5d0db2198c | ||
|
|
d81053ea38 | ||
|
|
8d39c3bc98 | ||
|
|
da351a3e32 | ||
|
|
c0591090f5 | ||
|
|
538aecb46e | ||
|
|
dbdbea85c2 | ||
|
|
ba2224dd06 | ||
|
|
44e2aa9183 | ||
|
|
202bff70fe | ||
|
|
26c0cd7f7c | ||
|
|
cb76301fbe | ||
|
|
8bfa12edf1 | ||
|
|
7daa969a5a | ||
|
|
4aeb60100d | ||
|
|
e2c7aaac5a | ||
|
|
6ff661c30d | ||
|
|
79066f8628 | ||
|
|
2c813a2692 | ||
|
|
d2cb595b83 | ||
|
|
cc4abcb00a | ||
|
|
c1ca85987f | ||
|
|
ecb5a0b8cc | ||
|
|
e12e8fc616 | ||
|
|
1fbbf32cd2 | ||
|
|
31edb15369 | ||
|
|
d7883d18d4 | ||
|
|
40100773d3 | ||
|
|
4048ed3a33 | ||
|
|
11f2d3cea7 | ||
|
|
aa656a39b8 | ||
|
|
e830d23533 | ||
|
|
9a666fb8cc | ||
|
|
0e208ed432 | ||
|
|
c8b769de8a | ||
|
|
c447655047 | ||
|
|
3ec9a1d869 | ||
|
|
d326886852 | ||
|
|
faef917cbd | ||
|
|
d27ba90c07 | ||
|
|
db4ca746e3 | ||
|
|
d50fbfb506 | ||
|
|
5d283a9f1f | ||
|
|
86fdc75feb | ||
|
|
b63231523a | ||
|
|
70e296674d | ||
|
|
5089fcd2f6 | ||
|
|
df2ce8ca6f | ||
|
|
7e209353bb | ||
|
|
c2806a94e2 | ||
|
|
d428120776 | ||
|
|
8c8493bc9d | ||
|
|
6b996ae57d | ||
|
|
ccfe1b13cb | ||
|
|
0c1c10bc66 | ||
|
|
fafd1801fe | ||
|
|
bcf6f665b8 | ||
|
|
bd069490b5 | ||
|
|
79d8d27b4c | ||
|
|
624b0b6372 | ||
|
|
7976cf5b3c | ||
|
|
440f52c943 | ||
|
|
47b1218a68 | ||
|
|
91ced056d2 | ||
|
|
8dace34e63 | ||
|
|
8182b0363f | ||
|
|
c5b036fedf | ||
|
|
e26ddd0ed5 | ||
|
|
ca83431e54 | ||
|
|
68a3e5a739 | ||
|
|
b98f10cb45 | ||
|
|
9730800b6a | ||
|
|
506276a2bd | ||
|
|
00c32e4b59 | ||
|
|
df56e6fe53 | ||
|
|
756641e837 | ||
|
|
05c2854dbc | ||
|
|
5c8aacdc17 | ||
|
|
745a5ab749 | ||
|
|
fe0dc4df88 | ||
|
|
33f2664fe9 | ||
|
|
a17e47fa43 | ||
|
|
877b46d2c1 | ||
|
|
cc7226ae9f | ||
|
|
bde975a3b9 | ||
|
|
f6f9024631 | ||
|
|
39aae34323 | ||
|
|
5630141ad7 | ||
|
|
535747e3f2 | ||
|
|
59a94943aa | ||
|
|
bf4889f238 | ||
|
|
7cc5afd798 | ||
|
|
11ab021672 | ||
|
|
feafd4bdae | ||
|
|
d6150645c0 | ||
|
|
ccd2cb44a2 | ||
|
|
ec5701459c | ||
|
|
ad8b68c998 | ||
|
|
c8066b01b6 | ||
|
|
ebd59f4dd3 | ||
|
|
109953ef49 | ||
|
|
124c7bcbb0 | ||
|
|
a0321aa6ff | ||
|
|
567feaac10 | ||
|
|
15c38e2f15 | ||
|
|
3c075e9542 | ||
|
|
9230969f43 | ||
|
|
0e16c67805 | ||
|
|
697e094a4e | ||
|
|
50d37798a2 | ||
|
|
e9d0676e75 | ||
|
|
7591906777 | ||
|
|
08671ed69c | ||
|
|
511d292e73 | ||
|
|
a413ae11cb | ||
|
|
833258f3d7 | ||
|
|
b8a1553368 | ||
|
|
058fe3e986 | ||
|
|
51ee83a427 | ||
|
|
5b21da7874 | ||
|
|
bd7f00bd9c | ||
|
|
517cca251f | ||
|
|
1033abd9fe | ||
|
|
113d022741 | ||
|
|
299a7b99ae | ||
|
|
66540ff86f | ||
|
|
8557558bd8 | ||
|
|
376cf08c71 | ||
|
|
83e5e650d2 | ||
|
|
b860ba2ee3 | ||
|
|
661fe1e649 | ||
|
|
5b8375f0a0 | ||
|
|
abe55fe950 | ||
|
|
4d4ddded6d | ||
|
|
1328708a70 | ||
|
|
85298319fa | ||
|
|
881feb1bd3 | ||
|
|
3e9fa63799 | ||
|
|
da2b190288 | ||
|
|
48d837c636 | ||
|
|
983407896c | ||
|
|
5c08bb810e | ||
|
|
17635da812 | ||
|
|
6d985866ee | ||
|
|
723137c0d4 | ||
|
|
938928865d | ||
|
|
d80b0cbf90 | ||
|
|
e88ef30ce6 | ||
|
|
4197c6f149 | ||
|
|
035f07877c | ||
|
|
4632be4fe5 | ||
|
|
b3d2b4cd37 | ||
|
|
c86fe9ada9 | ||
|
|
ecf93b7822 | ||
|
|
541b75ee6e | ||
|
|
77b08febdb | ||
|
|
fcda376f33 | ||
|
|
0848fc7e03 | ||
|
|
3bb8d6717f | ||
|
|
5e2496d59c | ||
|
|
c52da9d802 | ||
|
|
1d3dde32f2 | ||
|
|
0b999ce0e4 | ||
|
|
b04bd7069d | ||
|
|
249b0fbb32 | ||
|
|
41740fb45e | ||
|
|
0ad88508f7 | ||
|
|
8293b18278 | ||
|
|
2ba0364850 | ||
|
|
8b72043f33 | ||
|
|
2e7bc0b98a | ||
|
|
f0f9722ca6 | ||
|
|
b5ef88902b | ||
|
|
8278809383 | ||
|
|
4367459cf2 | ||
|
|
254132b83d | ||
|
|
7b466e6d0a | ||
|
|
7e6d4f5a3e | ||
|
|
ce099a297a | ||
|
|
949c848815 | ||
|
|
9bf9b9ea8c | ||
|
|
d8ed8b66f3 | ||
|
|
a131d39451 | ||
|
|
b540f58457 | ||
|
|
4f5a38b5c5 | ||
|
|
cefc3af08b | ||
|
|
e6ed50383c | ||
|
|
96facc103a | ||
|
|
407bbfb379 | ||
|
|
a99ebda513 | ||
|
|
537b604fc9 | ||
|
|
98bc570bf7 | ||
|
|
181b77c490 | ||
|
|
bc9eb82e6f | ||
|
|
29fc024ecd | ||
|
|
c1695d0910 | ||
|
|
6d6a4e79c9 | ||
|
|
417a3e1540 | ||
|
|
fa8c804d47 | ||
|
|
68392ce6f5 | ||
|
|
6873f62ad8 | ||
|
|
5f385e15f6 | ||
|
|
8c5d37b6ee | ||
|
|
9c3c2192dd | ||
|
|
4f9f73ca81 | ||
|
|
2c9a1f7b16 | ||
|
|
0ea4c1ac80 | ||
|
|
a873ec97eb | ||
|
|
cc8a65780e | ||
|
|
c117deb43b | ||
|
|
ae31d45c88 | ||
|
|
a0eb20ff1f | ||
|
|
34fe9981e4 | ||
|
|
291e91375f | ||
|
|
857f74b320 | ||
|
|
1d9608efc7 | ||
|
|
93616a4903 | ||
|
|
bb07206c55 | ||
|
|
2e5c0811e7 | ||
|
|
f6ac407e4d | ||
|
|
078c3135df | ||
|
|
92568c90c8 | ||
|
|
f1879c5fbc | ||
|
|
31bb770fdd | ||
|
|
e430f2658f | ||
|
|
3060175ff5 | ||
|
|
eb4233e2fd | ||
|
|
6b4c656849 | ||
|
|
1b8fada6aa | ||
|
|
7332c64964 | ||
|
|
977f9ee831 | ||
|
|
16fb3b49a5 | ||
|
|
3da1b3bf9b | ||
|
|
bc00856c05 | ||
|
|
52e3dece81 | ||
|
|
2c1d8fa18a | ||
|
|
3e34ae67f6 | ||
|
|
d6e16d0042 | ||
|
|
8e02d29ae6 | ||
|
|
ceebecec8d | ||
|
|
05d1eda422 | ||
|
|
31f318ad43 | ||
|
|
270f46e147 | ||
|
|
c0e9c37cc7 | ||
|
|
8564945713 | ||
|
|
7bd7f3fb73 | ||
|
|
5b5bfc8445 | ||
|
|
c466b6f9e7 | ||
|
|
407643c575 | ||
|
|
d9071ee9f1 | ||
|
|
97e118abfa | ||
|
|
412f091d76 | ||
|
|
d9278e9827 | ||
|
|
ca1f669e64 | ||
|
|
0298b1b3b7 | ||
|
|
4b1324de77 | ||
|
|
8e8dce9bec | ||
|
|
f4350522bf | ||
|
|
e2abb66a11 | ||
|
|
ab5fcab9bf | ||
|
|
cf547ef569 | ||
|
|
e75b386f7d | ||
|
|
796203859f | ||
|
|
40f68b70c1 | ||
|
|
40b2fe7339 | ||
|
|
a3b6d2d16e | ||
|
|
3983f8303f | ||
|
|
7cbd5e0ef6 | ||
|
|
dab9bb6575 | ||
|
|
7df85ea695 | ||
|
|
c132bda01c | ||
|
|
4e25bcfcdc | ||
|
|
ea463549c7 | ||
|
|
723acb31b3 | ||
|
|
5725db9234 | ||
|
|
8557e563bc | ||
|
|
d2491633ce | ||
|
|
002796e5f5 | ||
|
|
fa0accf251 | ||
|
|
dcb8176d90 | ||
|
|
be32b1a198 | ||
|
|
582e4acc11 | ||
|
|
10f75acf71 | ||
|
|
b9933f512f | ||
|
|
75a7f7ab22 | ||
|
|
757be2906e | ||
|
|
e214584c76 | ||
|
|
0bb6b498ce | ||
|
|
958d44a20d | ||
|
|
bb9424d944 | ||
|
|
11bf706aa2 | ||
|
|
033b8e6b36 | ||
|
|
7c3ea7b2ea | ||
|
|
a08043ae88 | ||
|
|
7c132a3ed5 | ||
|
|
20e774be1e | ||
|
|
6d6046757d | ||
|
|
55073b0a52 | ||
|
|
44eb4e51ed | ||
|
|
3cb042a49d | ||
|
|
b78ea7d24c | ||
|
|
c66728dce2 | ||
|
|
0be9a0cb88 | ||
|
|
a90f12dab7 | ||
|
|
ef33b004f9 | ||
|
|
2cac4b0d74 | ||
|
|
a49f516265 | ||
|
|
71ac26944d | ||
|
|
2d97fc1f59 | ||
|
|
9ef7743205 | ||
|
|
ee7ae11e90 | ||
|
|
f67d7f1db5 | ||
|
|
99981751a2 | ||
|
|
ffdf02c5df | ||
|
|
27c7d00a05 | ||
|
|
64c4137e5b | ||
|
|
8c26d0c6e6 | ||
|
|
81dcfd9f85 | ||
|
|
9334557fbf | ||
|
|
b09de8efce | ||
|
|
5a50eb56dd | ||
|
|
e49b257e94 | ||
|
|
b8a0f4e831 | ||
|
|
c265ea9847 | ||
|
|
29f8dcfb40 | ||
|
|
0c05983617 | ||
|
|
0bd653708c | ||
|
|
41d800cb63 | ||
|
|
cadc0bd509 | ||
|
|
b64da2710a | ||
|
|
82b08d0e3a | ||
|
|
8f77d1831b | ||
|
|
be722143e1 | ||
|
|
d8d974e2d7 | ||
|
|
9b7ca6f271 | ||
|
|
8ce018dbab | ||
|
|
180062c58c | ||
|
|
6076b8df69 | ||
|
|
5e65ee79b1 | ||
|
|
c0861c7362 | ||
|
|
37656f14d8 | ||
|
|
dec5535e54 | ||
|
|
1f0e3b157a | ||
|
|
d802e83f49 | ||
|
|
ebcae25762 | ||
|
|
5330267d16 | ||
|
|
892476973b | ||
|
|
84f4a25bc9 | ||
|
|
1460a88bb3 | ||
|
|
62e4c23961 | ||
|
|
d25ab35d58 | ||
|
|
a223cd90a1 | ||
|
|
aef92ba29c | ||
|
|
328d297490 | ||
|
|
3d240f3f18 | ||
|
|
45f35236a7 | ||
|
|
fba210f7ce | ||
|
|
8a09e5fc16 | ||
|
|
52e33e861c | ||
|
|
75d8824e6b | ||
|
|
325af677d3 | ||
|
|
1003e70b5e | ||
|
|
d70229201d | ||
|
|
823f91605b | ||
|
|
53f75034fc | ||
|
|
78649a5b54 | ||
|
|
f48db625a0 | ||
|
|
2ba66c4457 | ||
|
|
2c78ea1a4e | ||
|
|
73f50ac44e | ||
|
|
9ce48953c1 | ||
|
|
1098cd0c6b | ||
|
|
652ebd143c | ||
|
|
8e9d7c0f40 | ||
|
|
a64948a2ba | ||
|
|
43f619a081 | ||
|
|
a07de97df4 | ||
|
|
85d25068a8 | ||
|
|
7a0319cfe5 | ||
|
|
f750671f33 | ||
|
|
7886fe677a | ||
|
|
73c027f8e3 | ||
|
|
eda88cc462 | ||
|
|
652f4ebfed | ||
|
|
06a2f59bd0 | ||
|
|
0af57806da | ||
|
|
03f365e696 | ||
|
|
49a22674ba | ||
|
|
ec494511ec | ||
|
|
af02ce9c6e | ||
|
|
56e42859ab | ||
|
|
2d153359f8 | ||
|
|
068ce23716 | ||
|
|
03be2e3652 | ||
|
|
4ef2c0bed8 | ||
|
|
bfd405613c | ||
|
|
73e1c8c780 | ||
|
|
689ba1d4a2 | ||
|
|
39b9d00550 | ||
|
|
64f99d83a4 | ||
|
|
8f1faefa1c | ||
|
|
2c5ff9ada0 | ||
|
|
a9ceef5c37 | ||
|
|
c6f977ed4b | ||
|
|
cb240cd32a | ||
|
|
bc6349f823 | ||
|
|
a93a1ae40f | ||
|
|
25254255fe | ||
|
|
b0b2798f39 | ||
|
|
7f5c637aeb | ||
|
|
42634b500c | ||
|
|
6f0eb5eccd | ||
|
|
3d83891eb0 | ||
|
|
69a2a133d5 | ||
|
|
be4b38c76a | ||
|
|
7163b1132c | ||
|
|
3ccec1c996 | ||
|
|
47359dc8f1 | ||
|
|
43532c8455 | ||
|
|
d7c3d4ce52 | ||
|
|
ed7060a105 | ||
|
|
db0da4b741 | ||
|
|
c9c16968bb | ||
|
|
87420881c8 | ||
|
|
fdc598f2e1 | ||
|
|
f679145bd1 | ||
|
|
eeb161ec51 | ||
|
|
21cb7307d0 | ||
|
|
412a1eb7ee | ||
|
|
1d801acf72 | ||
|
|
0d7bbdad54 | ||
|
|
53b3d9cf9d | ||
|
|
c3ebbfb10e | ||
|
|
58f035e31a | ||
|
|
a8f1d98d40 | ||
|
|
cf6fa98433 | ||
|
|
937b3ca81d | ||
|
|
d0c5cf0d2d | ||
|
|
4cbf2bef82 | ||
|
|
388d808536 | ||
|
|
720aba3f2d | ||
|
|
f9101de956 | ||
|
|
bb04981280 | ||
|
|
57898ed6dd | ||
|
|
33b53e7605 | ||
|
|
9e8928aad9 | ||
|
|
89c71f9119 | ||
|
|
a4f6db6719 | ||
|
|
2d8e65ea32 | ||
|
|
48d1d27067 | ||
|
|
98aa597510 | ||
|
|
de56d48b2f | ||
|
|
4aeb9a7c56 | ||
|
|
b9b52b7c8b | ||
|
|
dc464a0b7b | ||
|
|
13b6079826 | ||
|
|
6f7dd10d95 | ||
|
|
24fb95291a | ||
|
|
48430bee60 | ||
|
|
42997dcb80 | ||
|
|
0ace189e38 | ||
|
|
d03a7911b5 | ||
|
|
84422676cb | ||
|
|
7441e3f4c5 | ||
|
|
f18132d674 | ||
|
|
5660007221 | ||
|
|
cfebf1dc4a | ||
|
|
5b0111b4c8 | ||
|
|
62a1d69cee | ||
|
|
86a6b04d4a | ||
|
|
8915950c7d | ||
|
|
641e349f33 | ||
|
|
72b4bf9c98 | ||
|
|
ccdeb3fbc8 | ||
|
|
34047fa60a | ||
|
|
05d483bc5b | ||
|
|
113efd9b16 | ||
|
|
c11a1f9679 | ||
|
|
2beeaa513b |
@@ -59,6 +59,11 @@ KeyboardMachine::Machine *MultiMachine::keyboard_machine() {
|
||||
}
|
||||
}
|
||||
|
||||
MouseMachine::Machine *MultiMachine::mouse_machine() {
|
||||
// TODO.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Configurable::Device *MultiMachine::configurable_device() {
|
||||
if(has_picked_) {
|
||||
return machines_.front()->configurable_device();
|
||||
|
||||
@@ -54,6 +54,7 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De
|
||||
Configurable::Device *configurable_device() override;
|
||||
CRTMachine::Machine *crt_machine() override;
|
||||
JoystickMachine::Machine *joystick_machine() override;
|
||||
MouseMachine::Machine *mouse_machine() override;
|
||||
KeyboardMachine::Machine *keyboard_machine() override;
|
||||
MediaTarget::Machine *media_target() override;
|
||||
void *raw_pointer() override;
|
||||
|
||||
@@ -17,6 +17,7 @@ enum class Machine {
|
||||
Atari2600,
|
||||
ColecoVision,
|
||||
Electron,
|
||||
Macintosh,
|
||||
MasterSystem,
|
||||
MSX,
|
||||
Oric,
|
||||
|
||||
@@ -290,6 +290,7 @@ Analyser::Static::TargetList Analyser::Static::MSX::GetTargets(const Media &medi
|
||||
target->region = target->media.tapes.empty() ? Target::Region::USA : Target::Region::Europe;
|
||||
|
||||
// Blindly accept disks for now.
|
||||
// TODO: how to spot an MSX disk?
|
||||
target->media.disks = media.disks;
|
||||
target->has_disk_drive = !media.disks.empty();
|
||||
|
||||
|
||||
26
Analyser/Static/Macintosh/StaticAnalyser.cpp
Normal file
26
Analyser/Static/Macintosh/StaticAnalyser.cpp
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// StaticAnalyser.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 02/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "StaticAnalyser.hpp"
|
||||
#include "Target.hpp"
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::Macintosh::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
// This analyser can comprehend disks only.
|
||||
if(media.disks.empty()) return {};
|
||||
|
||||
// If there is at least one disk, wave it through.
|
||||
Analyser::Static::TargetList targets;
|
||||
|
||||
using Target = Analyser::Static::Macintosh::Target;
|
||||
auto *target = new Target;
|
||||
target->machine = Analyser::Machine::Macintosh;
|
||||
target->media = media;
|
||||
targets.push_back(std::unique_ptr<Analyser::Static::Target>(target));
|
||||
|
||||
return targets;
|
||||
}
|
||||
27
Analyser/Static/Macintosh/StaticAnalyser.hpp
Normal file
27
Analyser/Static/Macintosh/StaticAnalyser.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// StaticAnalyser.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 02/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_Macintosh_StaticAnalyser_hpp
|
||||
#define Analyser_Static_Macintosh_StaticAnalyser_hpp
|
||||
|
||||
#include "../StaticAnalyser.hpp"
|
||||
#include "../../../Storage/TargetPlatforms.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Macintosh {
|
||||
|
||||
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif /* Analyser_Static_Macintosh_StaticAnalyser_hpp */
|
||||
31
Analyser/Static/Macintosh/Target.hpp
Normal file
31
Analyser/Static/Macintosh/Target.hpp
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// Target.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 03/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_Macintosh_Target_h
|
||||
#define Analyser_Static_Macintosh_Target_h
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Macintosh {
|
||||
|
||||
struct Target: public ::Analyser::Static::Target {
|
||||
enum class Model {
|
||||
Mac128k,
|
||||
Mac512k,
|
||||
Mac512ke,
|
||||
MacPlus
|
||||
};
|
||||
|
||||
Model model = Model::Mac512ke;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Analyser_Static_Macintosh_Target_h */
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "Coleco/StaticAnalyser.hpp"
|
||||
#include "Commodore/StaticAnalyser.hpp"
|
||||
#include "DiskII/StaticAnalyser.hpp"
|
||||
#include "Macintosh/StaticAnalyser.hpp"
|
||||
#include "MSX/StaticAnalyser.hpp"
|
||||
#include "Oric/StaticAnalyser.hpp"
|
||||
#include "Sega/StaticAnalyser.hpp"
|
||||
@@ -35,6 +36,7 @@
|
||||
#include "../../Storage/Disk/DiskImage/Formats/AppleDSK.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/CPCDSK.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/D64.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/MacintoshIMG.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/G64.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/DMK.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/HFE.hpp"
|
||||
@@ -85,34 +87,37 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
TryInsert(list, class, platforms) \
|
||||
}
|
||||
|
||||
Format("80", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 80
|
||||
Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 81
|
||||
Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600) // A26
|
||||
Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn) // ADF
|
||||
Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge) // BIN
|
||||
Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX) // CAS
|
||||
Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC) // CDT
|
||||
Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision) // COL
|
||||
Format("csw", result.tapes, Tape::CSW, TargetPlatform::AllTape) // CSW
|
||||
Format("d64", result.disks, Disk::DiskImageHolder<Storage::Disk::D64>, TargetPlatform::Commodore) // D64
|
||||
Format("dmk", result.disks, Disk::DiskImageHolder<Storage::Disk::DMK>, TargetPlatform::MSX) // DMK
|
||||
Format("do", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DO
|
||||
Format("dsd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // DSD
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::CPCDSK>, TargetPlatform::AmstradCPC) // DSK (Amstrad CPC)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DSK (Apple)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MSXDSK>, TargetPlatform::MSX) // DSK (MSX)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric) // DSK (Oric)
|
||||
Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore) // G64
|
||||
Format("80", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 80
|
||||
Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 81
|
||||
Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600) // A26
|
||||
Format("adf", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, TargetPlatform::Acorn) // ADF
|
||||
Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge) // BIN (cartridge dump)
|
||||
Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX) // CAS
|
||||
Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC) // CDT
|
||||
Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision) // COL
|
||||
Format("csw", result.tapes, Tape::CSW, TargetPlatform::AllTape) // CSW
|
||||
Format("d64", result.disks, Disk::DiskImageHolder<Storage::Disk::D64>, TargetPlatform::Commodore) // D64
|
||||
Format("dmk", result.disks, Disk::DiskImageHolder<Storage::Disk::DMK>, TargetPlatform::MSX) // DMK
|
||||
Format("do", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DO
|
||||
Format("dsd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // DSD
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::CPCDSK>, TargetPlatform::AmstradCPC) // DSK (Amstrad CPC)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DSK (Apple II)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // DSK (Macintosh)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MSXDSK>, TargetPlatform::MSX) // DSK (MSX)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric) // DSK (Oric)
|
||||
Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore) // G64
|
||||
Format( "hfe",
|
||||
result.disks,
|
||||
Disk::DiskImageHolder<Storage::Disk::HFE>,
|
||||
TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric)
|
||||
// HFE (TODO: switch to AllDisk once the MSX stops being so greedy)
|
||||
Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII) // NIB
|
||||
Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // O
|
||||
Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P
|
||||
Format("po", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // PO
|
||||
Format("p81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P81
|
||||
Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
|
||||
Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
|
||||
Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII) // NIB
|
||||
Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // O
|
||||
Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P
|
||||
Format("po", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // PO
|
||||
Format("p81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P81
|
||||
|
||||
// PRG
|
||||
if(extension == "prg") {
|
||||
@@ -129,16 +134,16 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
Format( "rom",
|
||||
result.cartridges,
|
||||
Cartridge::BinaryDump,
|
||||
TargetPlatform::AcornElectron | TargetPlatform::ColecoVision | TargetPlatform::MSX) // ROM
|
||||
Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SG
|
||||
Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS
|
||||
Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // SSD
|
||||
Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore)
|
||||
Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric)
|
||||
Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX) // TSX
|
||||
Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081) // TZX
|
||||
Format("uef", result.tapes, Tape::UEF, TargetPlatform::Acorn) // UEF (tape)
|
||||
Format("woz", result.disks, Disk::DiskImageHolder<Storage::Disk::WOZ>, TargetPlatform::DiskII) // WOZ
|
||||
TargetPlatform::AcornElectron | TargetPlatform::ColecoVision | TargetPlatform::MSX) // ROM
|
||||
Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SG
|
||||
Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS
|
||||
Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // SSD
|
||||
Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore)
|
||||
Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric)
|
||||
Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX) // TSX
|
||||
Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081) // TZX
|
||||
Format("uef", result.tapes, Tape::UEF, TargetPlatform::Acorn) // UEF (tape)
|
||||
Format("woz", result.disks, Disk::DiskImageHolder<Storage::Disk::WOZ>, TargetPlatform::DiskII) // WOZ
|
||||
|
||||
#undef Format
|
||||
#undef Insert
|
||||
@@ -173,9 +178,10 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) {
|
||||
if(potential_platforms & TargetPlatform::ColecoVision) Append(Coleco);
|
||||
if(potential_platforms & TargetPlatform::Commodore) Append(Commodore);
|
||||
if(potential_platforms & TargetPlatform::DiskII) Append(DiskII);
|
||||
if(potential_platforms & TargetPlatform::Sega) Append(Sega);
|
||||
if(potential_platforms & TargetPlatform::Macintosh) Append(Macintosh);
|
||||
if(potential_platforms & TargetPlatform::MSX) Append(MSX);
|
||||
if(potential_platforms & TargetPlatform::Oric) Append(Oric);
|
||||
if(potential_platforms & TargetPlatform::Sega) Append(Sega);
|
||||
if(potential_platforms & TargetPlatform::ZX8081) Append(ZX8081);
|
||||
#undef Append
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#ifndef ClockReceiver_hpp
|
||||
#define ClockReceiver_hpp
|
||||
|
||||
#include "ForceInline.hpp"
|
||||
|
||||
/*
|
||||
Informal pattern for all classes that run from a clock cycle:
|
||||
|
||||
@@ -52,79 +54,92 @@
|
||||
*/
|
||||
template <class T> class WrappedInt {
|
||||
public:
|
||||
constexpr WrappedInt(int l) : length_(l) {}
|
||||
constexpr WrappedInt() : length_(0) {}
|
||||
forceinline constexpr WrappedInt(int l) noexcept : length_(l) {}
|
||||
forceinline constexpr WrappedInt() noexcept : length_(0) {}
|
||||
|
||||
T &operator =(const T &rhs) {
|
||||
forceinline T &operator =(const T &rhs) {
|
||||
length_ = rhs.length_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
T &operator +=(const T &rhs) {
|
||||
forceinline T &operator +=(const T &rhs) {
|
||||
length_ += rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator -=(const T &rhs) {
|
||||
forceinline T &operator -=(const T &rhs) {
|
||||
length_ -= rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator ++() {
|
||||
forceinline T &operator ++() {
|
||||
++ length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator ++(int) {
|
||||
forceinline T &operator ++(int) {
|
||||
length_ ++;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator --() {
|
||||
forceinline T &operator --() {
|
||||
-- length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator --(int) {
|
||||
forceinline T &operator --(int) {
|
||||
length_ --;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator %=(const T &rhs) {
|
||||
forceinline T &operator *=(const T &rhs) {
|
||||
length_ *= rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
forceinline T &operator /=(const T &rhs) {
|
||||
length_ /= rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
forceinline T &operator %=(const T &rhs) {
|
||||
length_ %= rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
T &operator &=(const T &rhs) {
|
||||
forceinline T &operator &=(const T &rhs) {
|
||||
length_ &= rhs.length_;
|
||||
return *static_cast<T *>(this);
|
||||
}
|
||||
|
||||
constexpr T operator +(const T &rhs) const { return T(length_ + rhs.length_); }
|
||||
constexpr T operator -(const T &rhs) const { return T(length_ - rhs.length_); }
|
||||
forceinline constexpr T operator +(const T &rhs) const { return T(length_ + rhs.length_); }
|
||||
forceinline constexpr T operator -(const T &rhs) const { return T(length_ - rhs.length_); }
|
||||
|
||||
constexpr T operator %(const T &rhs) const { return T(length_ % rhs.length_); }
|
||||
constexpr T operator &(const T &rhs) const { return T(length_ & rhs.length_); }
|
||||
forceinline constexpr T operator *(const T &rhs) const { return T(length_ * rhs.length_); }
|
||||
forceinline constexpr T operator /(const T &rhs) const { return T(length_ / rhs.length_); }
|
||||
|
||||
constexpr T operator -() const { return T(- length_); }
|
||||
forceinline constexpr T operator %(const T &rhs) const { return T(length_ % rhs.length_); }
|
||||
forceinline constexpr T operator &(const T &rhs) const { return T(length_ & rhs.length_); }
|
||||
|
||||
constexpr bool operator <(const T &rhs) const { return length_ < rhs.length_; }
|
||||
constexpr bool operator >(const T &rhs) const { return length_ > rhs.length_; }
|
||||
constexpr bool operator <=(const T &rhs) const { return length_ <= rhs.length_; }
|
||||
constexpr bool operator >=(const T &rhs) const { return length_ >= rhs.length_; }
|
||||
constexpr bool operator ==(const T &rhs) const { return length_ == rhs.length_; }
|
||||
constexpr bool operator !=(const T &rhs) const { return length_ != rhs.length_; }
|
||||
forceinline constexpr T operator -() const { return T(- length_); }
|
||||
|
||||
constexpr bool operator !() const { return !length_; }
|
||||
forceinline constexpr bool operator <(const T &rhs) const { return length_ < rhs.length_; }
|
||||
forceinline constexpr bool operator >(const T &rhs) const { return length_ > rhs.length_; }
|
||||
forceinline constexpr bool operator <=(const T &rhs) const { return length_ <= rhs.length_; }
|
||||
forceinline constexpr bool operator >=(const T &rhs) const { return length_ >= rhs.length_; }
|
||||
forceinline constexpr bool operator ==(const T &rhs) const { return length_ == rhs.length_; }
|
||||
forceinline constexpr bool operator !=(const T &rhs) const { return length_ != rhs.length_; }
|
||||
|
||||
forceinline constexpr bool operator !() const { return !length_; }
|
||||
// bool operator () is not supported because it offers an implicit cast to int, which is prone silently to permit misuse
|
||||
|
||||
constexpr int as_int() const { return length_; }
|
||||
forceinline constexpr int as_int() const { return length_; }
|
||||
|
||||
/*!
|
||||
Severs from @c this the effect of dividing by @c divisor; @c this will end up with
|
||||
the value of @c this modulo @c divisor and @c divided by @c divisor is returned.
|
||||
*/
|
||||
T divide(const T &divisor) {
|
||||
forceinline T divide(const T &divisor) {
|
||||
T result(length_ / divisor.length_);
|
||||
length_ %= divisor.length_;
|
||||
return result;
|
||||
@@ -134,10 +149,12 @@ template <class T> class WrappedInt {
|
||||
Flushes the value in @c this. The current value is returned, and the internal value
|
||||
is reset to zero.
|
||||
*/
|
||||
T flush() {
|
||||
T result(length_);
|
||||
length_ = 0;
|
||||
return result;
|
||||
template <typename Result> Result flush() {
|
||||
// Jiggery pokery here; switching to function overloading avoids
|
||||
// the namespace-level requirement for template specialisation.
|
||||
Result r;
|
||||
static_cast<T *>(this)->fill(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
// operator int() is deliberately not provided, to avoid accidental subtitution of
|
||||
@@ -150,51 +167,59 @@ template <class T> class WrappedInt {
|
||||
/// Describes an integer number of whole cycles: pairs of clock signal transitions.
|
||||
class Cycles: public WrappedInt<Cycles> {
|
||||
public:
|
||||
constexpr Cycles(int l) : WrappedInt<Cycles>(l) {}
|
||||
constexpr Cycles() : WrappedInt<Cycles>() {}
|
||||
constexpr Cycles(const Cycles &cycles) : WrappedInt<Cycles>(cycles.length_) {}
|
||||
forceinline constexpr Cycles(int l) noexcept : WrappedInt<Cycles>(l) {}
|
||||
forceinline constexpr Cycles() noexcept : WrappedInt<Cycles>() {}
|
||||
forceinline constexpr Cycles(const Cycles &cycles) noexcept : WrappedInt<Cycles>(cycles.length_) {}
|
||||
|
||||
private:
|
||||
friend WrappedInt;
|
||||
void fill(Cycles &result) {
|
||||
result.length_ = length_;
|
||||
length_ = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Describes an integer number of half cycles: single clock signal transitions.
|
||||
class HalfCycles: public WrappedInt<HalfCycles> {
|
||||
public:
|
||||
constexpr HalfCycles(int l) : WrappedInt<HalfCycles>(l) {}
|
||||
constexpr HalfCycles() : WrappedInt<HalfCycles>() {}
|
||||
forceinline constexpr HalfCycles(int l) noexcept : WrappedInt<HalfCycles>(l) {}
|
||||
forceinline constexpr HalfCycles() noexcept : WrappedInt<HalfCycles>() {}
|
||||
|
||||
constexpr HalfCycles(const Cycles cycles) : WrappedInt<HalfCycles>(cycles.as_int() * 2) {}
|
||||
constexpr HalfCycles(const HalfCycles &half_cycles) : WrappedInt<HalfCycles>(half_cycles.length_) {}
|
||||
forceinline constexpr HalfCycles(const Cycles &cycles) noexcept : WrappedInt<HalfCycles>(cycles.as_int() * 2) {}
|
||||
forceinline constexpr HalfCycles(const HalfCycles &half_cycles) noexcept : WrappedInt<HalfCycles>(half_cycles.length_) {}
|
||||
|
||||
/// @returns The number of whole cycles completely covered by this span of half cycles.
|
||||
constexpr Cycles cycles() const {
|
||||
forceinline constexpr Cycles cycles() const {
|
||||
return Cycles(length_ >> 1);
|
||||
}
|
||||
|
||||
/// Flushes the whole cycles in @c this, subtracting that many from the total stored here.
|
||||
Cycles flush_cycles() {
|
||||
Cycles result(length_ >> 1);
|
||||
length_ &= 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Flushes the half cycles in @c this, returning the number stored and setting this total to zero.
|
||||
HalfCycles flush() {
|
||||
HalfCycles result(length_);
|
||||
length_ = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
Severs from @c this the effect of dividing by @c divisor; @c this will end up with
|
||||
the value of @c this modulo @c divisor and @c divided by @c divisor is returned.
|
||||
*/
|
||||
Cycles divide_cycles(const Cycles &divisor) {
|
||||
HalfCycles half_divisor = HalfCycles(divisor);
|
||||
Cycles result(length_ / half_divisor.length_);
|
||||
forceinline Cycles divide_cycles(const Cycles &divisor) {
|
||||
const HalfCycles half_divisor = HalfCycles(divisor);
|
||||
const Cycles result(length_ / half_divisor.length_);
|
||||
length_ %= half_divisor.length_;
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
friend WrappedInt;
|
||||
void fill(Cycles &result) {
|
||||
result = Cycles(length_ >> 1);
|
||||
length_ &= 1;
|
||||
}
|
||||
|
||||
void fill(HalfCycles &result) {
|
||||
result.length_ = length_;
|
||||
length_ = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Create a specialisation of WrappedInt::flush for converting HalfCycles to Cycles
|
||||
// without losing the fractional part.
|
||||
|
||||
/*!
|
||||
If a component implements only run_for(Cycles), an owner can wrap it in HalfClockReceiver
|
||||
automatically to gain run_for(HalfCycles).
|
||||
@@ -203,9 +228,9 @@ template <class T> class HalfClockReceiver: public T {
|
||||
public:
|
||||
using T::T;
|
||||
|
||||
inline void run_for(const HalfCycles half_cycles) {
|
||||
forceinline void run_for(const HalfCycles half_cycles) {
|
||||
half_cycles_ += half_cycles;
|
||||
T::run_for(half_cycles_.flush_cycles());
|
||||
T::run_for(half_cycles_.flush<Cycles>());
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
//
|
||||
// ClockDeferrer.hpp
|
||||
// DeferredQueue.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 23/08/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef ClockDeferrer_h
|
||||
#define ClockDeferrer_h
|
||||
#ifndef DeferredQueue_h
|
||||
#define DeferredQueue_h
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
/*!
|
||||
A ClockDeferrer maintains a list of ordered actions and the times at which
|
||||
A DeferredQueue maintains a list of ordered actions and the times at which
|
||||
they should happen, and divides a total execution period up into the portions
|
||||
that occur between those actions, triggering each action when it is reached.
|
||||
*/
|
||||
template <typename TimeUnit> class ClockDeferrer {
|
||||
template <typename TimeUnit> class DeferredQueue {
|
||||
public:
|
||||
/// Constructs a ClockDeferrer that will call target(period) in between deferred actions.
|
||||
ClockDeferrer(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {}
|
||||
/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
|
||||
DeferredQueue(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {}
|
||||
|
||||
/*!
|
||||
Schedules @c action to occur in @c delay units of time.
|
||||
@@ -79,4 +79,4 @@ template <typename TimeUnit> class ClockDeferrer {
|
||||
std::vector<DeferredAction> pending_actions_;
|
||||
};
|
||||
|
||||
#endif /* ClockDeferrer_h */
|
||||
#endif /* DeferredQueue_h */
|
||||
110
ClockReceiver/JustInTime.hpp
Normal file
110
ClockReceiver/JustInTime.hpp
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// JustInTime.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 28/07/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef JustInTime_h
|
||||
#define JustInTime_h
|
||||
|
||||
#include "../Concurrency/AsyncTaskQueue.hpp"
|
||||
|
||||
/*!
|
||||
A JustInTimeActor holds (i) an embedded object with a run_for method; and (ii) an amount
|
||||
of time since run_for was last called.
|
||||
|
||||
Time can be added using the += operator. The -> operator can be used to access the
|
||||
embedded object. All time accumulated will be pushed to object before the pointer is returned.
|
||||
|
||||
Machines that accumulate HalfCycle time but supply to a Cycle-counted device may supply a
|
||||
separate @c TargetTimeScale at template declaration.
|
||||
*/
|
||||
template <class T, class LocalTimeScale, class TargetTimeScale = LocalTimeScale> class JustInTimeActor {
|
||||
public:
|
||||
/// Constructs a new JustInTimeActor using the same construction arguments as the included object.
|
||||
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
||||
|
||||
/// Adds time to the actor.
|
||||
inline void operator += (const LocalTimeScale &rhs) {
|
||||
time_since_update_ += rhs;
|
||||
is_flushed_ = false;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time and returns a pointer to the included object.
|
||||
inline T *operator->() {
|
||||
flush();
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Returns a pointer to the included object without flushing time.
|
||||
inline T *last_valid() {
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time.
|
||||
inline void flush() {
|
||||
if(!is_flushed_) object_.run_for(time_since_update_.template flush<TargetTimeScale>());
|
||||
is_flushed_ = true;
|
||||
}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale time_since_update_;
|
||||
bool is_flushed_ = true;
|
||||
};
|
||||
|
||||
/*!
|
||||
A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
|
||||
Any time the amount of accumulated time crosses a threshold provided at construction time,
|
||||
the object will be updated on the AsyncTaskQueue.
|
||||
*/
|
||||
template <class T, class LocalTimeScale, class TargetTimeScale = LocalTimeScale> class AsyncJustInTimeActor {
|
||||
public:
|
||||
/// Constructs a new AsyncJustInTimeActor using the same construction arguments as the included object.
|
||||
template<typename... Args> AsyncJustInTimeActor(TargetTimeScale threshold, Args&&... args) :
|
||||
object_(std::forward<Args>(args)...),
|
||||
threshold_(threshold) {}
|
||||
|
||||
/// Adds time to the actor.
|
||||
inline void operator += (const LocalTimeScale &rhs) {
|
||||
time_since_update_ += rhs;
|
||||
if(time_since_update_ >= threshold_) {
|
||||
time_since_update_ -= threshold_;
|
||||
task_queue_.enqueue([this] () {
|
||||
object_.run_for(threshold_);
|
||||
});
|
||||
}
|
||||
is_flushed_ = false;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time and returns a pointer to the included object.
|
||||
inline T *operator->() {
|
||||
flush();
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Returns a pointer to the included object without flushing time.
|
||||
inline T *last_valid() {
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time.
|
||||
inline void flush() {
|
||||
if(!is_flushed_) {
|
||||
task_queue_.flush();
|
||||
object_.run_for(time_since_update_.template flush<TargetTimeScale>());
|
||||
is_flushed_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale time_since_update_;
|
||||
TargetTimeScale threshold_;
|
||||
bool is_flushed_ = true;
|
||||
Concurrency::AsyncTaskQueue task_queue_;
|
||||
};
|
||||
|
||||
#endif /* JustInTime_h */
|
||||
@@ -47,6 +47,12 @@ class PortHandler {
|
||||
|
||||
/// Sets the current logical value of the interrupt line.
|
||||
void set_interrupt_status(bool status) {}
|
||||
|
||||
/// Provides a measure of time elapsed between other calls.
|
||||
void run_for(HalfCycles duration) {}
|
||||
|
||||
/// Receives passed-on flush() calls from the 6522.
|
||||
void flush() {}
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -71,26 +77,6 @@ class IRQDelegatePortHandler: public PortHandler {
|
||||
Delegate *delegate_ = nullptr;
|
||||
};
|
||||
|
||||
class MOS6522Base: public MOS6522Storage {
|
||||
public:
|
||||
/// Sets the input value of line @c line on port @c port.
|
||||
void set_control_line_input(Port port, Line line, bool value);
|
||||
|
||||
/// Runs for a specified number of half cycles.
|
||||
void run_for(const HalfCycles half_cycles);
|
||||
|
||||
/// Runs for a specified number of cycles.
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
/// @returns @c true if the IRQ line is currently active; @c false otherwise.
|
||||
bool get_interrupt_line();
|
||||
|
||||
private:
|
||||
inline void do_phase1();
|
||||
inline void do_phase2();
|
||||
virtual void reevaluate_interrupts() = 0;
|
||||
};
|
||||
|
||||
/*!
|
||||
Implements a template for emulation of the MOS 6522 Versatile Interface Adaptor ('VIA').
|
||||
|
||||
@@ -102,7 +88,7 @@ class MOS6522Base: public MOS6522Storage {
|
||||
Consumers should derive their own curiously-recurring-template-pattern subclass,
|
||||
implementing bus communications as required.
|
||||
*/
|
||||
template <class T> class MOS6522: public MOS6522Base {
|
||||
template <class T> class MOS6522: public MOS6522Storage {
|
||||
public:
|
||||
MOS6522(T &bus_handler) noexcept : bus_handler_(bus_handler) {}
|
||||
MOS6522(const MOS6522 &) = delete;
|
||||
@@ -116,11 +102,39 @@ template <class T> class MOS6522: public MOS6522Base {
|
||||
/*! @returns the bus handler. */
|
||||
T &bus_handler();
|
||||
|
||||
/// Sets the input value of line @c line on port @c port.
|
||||
void set_control_line_input(Port port, Line line, bool value);
|
||||
|
||||
/// Runs for a specified number of half cycles.
|
||||
void run_for(const HalfCycles half_cycles);
|
||||
|
||||
/// Runs for a specified number of cycles.
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
/// @returns @c true if the IRQ line is currently active; @c false otherwise.
|
||||
bool get_interrupt_line();
|
||||
|
||||
/// Updates the port handler to the current time and then requests that it flush.
|
||||
void flush();
|
||||
|
||||
private:
|
||||
void do_phase1();
|
||||
void do_phase2();
|
||||
void shift_in();
|
||||
void shift_out();
|
||||
|
||||
T &bus_handler_;
|
||||
HalfCycles time_since_bus_handler_call_;
|
||||
|
||||
void access(int address);
|
||||
|
||||
uint8_t get_port_input(Port port, uint8_t output_mask, uint8_t output);
|
||||
inline void reevaluate_interrupts();
|
||||
|
||||
/// Sets the current intended output value for the port and line;
|
||||
/// if this affects the visible output, it will be passed to the handler.
|
||||
void set_control_line_output(Port port, Line line, LineState value);
|
||||
void evaluate_cb2_output();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
//
|
||||
// 6522Base.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/09/2017.
|
||||
// Copyright 2017 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "../6522.hpp"
|
||||
|
||||
using namespace MOS::MOS6522;
|
||||
|
||||
void MOS6522Base::set_control_line_input(Port port, Line line, bool value) {
|
||||
switch(line) {
|
||||
case Line::One:
|
||||
if( value != control_inputs_[port].line_one &&
|
||||
value == !!(registers_.peripheral_control & (port ? 0x10 : 0x01))
|
||||
) {
|
||||
registers_.interrupt_flags |= port ? InterruptFlag::CB1ActiveEdge : InterruptFlag::CA1ActiveEdge;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
control_inputs_[port].line_one = value;
|
||||
break;
|
||||
|
||||
case Line::Two:
|
||||
// TODO: output modes, but probably elsewhere?
|
||||
if( value != control_inputs_[port].line_two && // i.e. value has changed ...
|
||||
!(registers_.peripheral_control & (port ? 0x80 : 0x08)) && // ... and line is input ...
|
||||
value == !!(registers_.peripheral_control & (port ? 0x40 : 0x04)) // ... and it's either high or low, as required
|
||||
) {
|
||||
registers_.interrupt_flags |= port ? InterruptFlag::CB2ActiveEdge : InterruptFlag::CA2ActiveEdge;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
control_inputs_[port].line_two = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MOS6522Base::do_phase2() {
|
||||
registers_.last_timer[0] = registers_.timer[0];
|
||||
registers_.last_timer[1] = registers_.timer[1];
|
||||
|
||||
if(registers_.timer_needs_reload) {
|
||||
registers_.timer_needs_reload = false;
|
||||
registers_.timer[0] = registers_.timer_latch[0];
|
||||
} else {
|
||||
registers_.timer[0] --;
|
||||
}
|
||||
|
||||
registers_.timer[1] --;
|
||||
if(registers_.next_timer[0] >= 0) {
|
||||
registers_.timer[0] = static_cast<uint16_t>(registers_.next_timer[0]);
|
||||
registers_.next_timer[0] = -1;
|
||||
}
|
||||
if(registers_.next_timer[1] >= 0) {
|
||||
registers_.timer[1] = static_cast<uint16_t>(registers_.next_timer[1]);
|
||||
registers_.next_timer[1] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void MOS6522Base::do_phase1() {
|
||||
// IRQ is raised on the half cycle after overflow
|
||||
if((registers_.timer[1] == 0xffff) && !registers_.last_timer[1] && timer_is_running_[1]) {
|
||||
timer_is_running_[1] = false;
|
||||
registers_.interrupt_flags |= InterruptFlag::Timer2;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
|
||||
if((registers_.timer[0] == 0xffff) && !registers_.last_timer[0] && timer_is_running_[0]) {
|
||||
registers_.interrupt_flags |= InterruptFlag::Timer1;
|
||||
reevaluate_interrupts();
|
||||
|
||||
if(registers_.auxiliary_control&0x40)
|
||||
registers_.timer_needs_reload = true;
|
||||
else
|
||||
timer_is_running_[0] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/*! Runs for a specified number of half cycles. */
|
||||
void MOS6522Base::run_for(const HalfCycles half_cycles) {
|
||||
int number_of_half_cycles = half_cycles.as_int();
|
||||
|
||||
if(is_phase2_) {
|
||||
do_phase2();
|
||||
number_of_half_cycles--;
|
||||
}
|
||||
|
||||
while(number_of_half_cycles >= 2) {
|
||||
do_phase1();
|
||||
do_phase2();
|
||||
number_of_half_cycles -= 2;
|
||||
}
|
||||
|
||||
if(number_of_half_cycles) {
|
||||
do_phase1();
|
||||
is_phase2_ = true;
|
||||
} else {
|
||||
is_phase2_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
/*! Runs for a specified number of cycles. */
|
||||
void MOS6522Base::run_for(const Cycles cycles) {
|
||||
int number_of_cycles = cycles.as_int();
|
||||
while(number_of_cycles--) {
|
||||
do_phase1();
|
||||
do_phase2();
|
||||
}
|
||||
}
|
||||
|
||||
/*! @returns @c true if the IRQ line is currently active; @c false otherwise. */
|
||||
bool MOS6522Base::get_interrupt_line() {
|
||||
uint8_t interrupt_status = registers_.interrupt_flags & registers_.interrupt_enable & 0x7f;
|
||||
return !!interrupt_status;
|
||||
}
|
||||
@@ -11,29 +11,58 @@
|
||||
namespace MOS {
|
||||
namespace MOS6522 {
|
||||
|
||||
template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) {
|
||||
address &= 0xf;
|
||||
template <typename T> void MOS6522<T>::access(int address) {
|
||||
switch(address) {
|
||||
case 0x0:
|
||||
// In both handshake and pulse modes, CB2 goes low on any read or write of Port B.
|
||||
if(handshake_modes_[1] != HandshakeMode::None) {
|
||||
set_control_line_output(Port::B, Line::Two, LineState::Off);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xf:
|
||||
case 0x1:
|
||||
// In both handshake and pulse modes, CA2 goes low on any read or write of Port A.
|
||||
if(handshake_modes_[0] != HandshakeMode::None) {
|
||||
set_control_line_output(Port::A, Line::Two, LineState::Off);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) {
|
||||
address &= 0xf;
|
||||
access(address);
|
||||
switch(address) {
|
||||
case 0x0: // Write Port B.
|
||||
// Store locally and communicate outwards.
|
||||
registers_.output[1] = value;
|
||||
bus_handler_.set_port_output(Port::B, value, registers_.data_direction[1]); // TODO: handshake
|
||||
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_port_output(Port::B, value, registers_.data_direction[1]);
|
||||
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | ((registers_.peripheral_control&0x20) ? 0 : InterruptFlag::CB2ActiveEdge));
|
||||
reevaluate_interrupts();
|
||||
break;
|
||||
case 0xf:
|
||||
case 0x1:
|
||||
case 0x1: // Write Port A.
|
||||
registers_.output[0] = value;
|
||||
bus_handler_.set_port_output(Port::A, value, registers_.data_direction[0]); // TODO: handshake
|
||||
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_port_output(Port::A, value, registers_.data_direction[0]);
|
||||
|
||||
if(handshake_modes_[1] != HandshakeMode::None) {
|
||||
set_control_line_output(Port::A, Line::Two, LineState::Off);
|
||||
}
|
||||
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | ((registers_.peripheral_control&0x02) ? 0 : InterruptFlag::CB2ActiveEdge));
|
||||
reevaluate_interrupts();
|
||||
break;
|
||||
|
||||
case 0x2:
|
||||
case 0x2: // Port B direction.
|
||||
registers_.data_direction[1] = value;
|
||||
break;
|
||||
case 0x3:
|
||||
case 0x3: // Port A direction.
|
||||
registers_.data_direction[0] = value;
|
||||
break;
|
||||
|
||||
@@ -59,31 +88,57 @@ template <typename T> void MOS6522<T>::set_register(int address, uint8_t value)
|
||||
break;
|
||||
|
||||
// Shift
|
||||
case 0xa: registers_.shift = value; break;
|
||||
case 0xa:
|
||||
registers_.shift = value;
|
||||
shift_bits_remaining_ = 8;
|
||||
registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister;
|
||||
reevaluate_interrupts();
|
||||
break;
|
||||
|
||||
// Control
|
||||
case 0xb:
|
||||
registers_.auxiliary_control = value;
|
||||
evaluate_cb2_output();
|
||||
break;
|
||||
case 0xc:
|
||||
case 0xc: {
|
||||
// const auto old_peripheral_control = registers_.peripheral_control;
|
||||
registers_.peripheral_control = value;
|
||||
|
||||
// TODO: simplify below; trying to avoid improper logging of unimplemented warnings in input mode
|
||||
if(value & 0x08) {
|
||||
switch(value & 0x0e) {
|
||||
default: LOG("Unimplemented control line mode " << int((value >> 1)&7)); break;
|
||||
case 0x0c: bus_handler_.set_control_line_output(Port::A, Line::Two, false); break;
|
||||
case 0x0e: bus_handler_.set_control_line_output(Port::A, Line::Two, true); break;
|
||||
int shift = 0;
|
||||
for(int port = 0; port < 2; ++port) {
|
||||
handshake_modes_[port] = HandshakeMode::None;
|
||||
switch((value >> shift) & 0x0e) {
|
||||
default: break;
|
||||
|
||||
case 0x00: // Negative interrupt input; set Cx2 interrupt on negative Cx2 transition, clear on access to Port x register.
|
||||
case 0x02: // Independent negative interrupt input; set Cx2 interrupt on negative transition, don't clear automatically.
|
||||
case 0x04: // Positive interrupt input; set Cx2 interrupt on positive Cx2 transition, clear on access to Port x register.
|
||||
case 0x06: // Independent positive interrupt input; set Cx2 interrupt on positive transition, don't clear automatically.
|
||||
set_control_line_output(Port(port), Line::Two, LineState::Input);
|
||||
break;
|
||||
|
||||
case 0x08: // Handshake: set Cx2 to low on any read or write of Port x; set to high on an active transition of Cx1.
|
||||
handshake_modes_[port] = HandshakeMode::Handshake;
|
||||
set_control_line_output(Port(port), Line::Two, LineState::Off); // At a guess.
|
||||
break;
|
||||
|
||||
case 0x0a: // Pulse output: Cx2 is low for one cycle following a read or write of Port x.
|
||||
handshake_modes_[port] = HandshakeMode::Pulse;
|
||||
set_control_line_output(Port(port), Line::Two, LineState::On);
|
||||
break;
|
||||
|
||||
case 0x0c: // Manual output: Cx2 low.
|
||||
set_control_line_output(Port(port), Line::Two, LineState::Off);
|
||||
break;
|
||||
|
||||
case 0x0e: // Manual output: Cx2 high.
|
||||
set_control_line_output(Port(port), Line::Two, LineState::On);
|
||||
break;
|
||||
}
|
||||
|
||||
shift += 4;
|
||||
}
|
||||
if(value & 0x80) {
|
||||
switch(value & 0xe0) {
|
||||
default: LOG("Unimplemented control line mode " << int((value >> 5)&7)); break;
|
||||
case 0xc0: bus_handler_.set_control_line_output(Port::B, Line::Two, false); break;
|
||||
case 0xe0: bus_handler_.set_control_line_output(Port::B, Line::Two, true); break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
} break;
|
||||
|
||||
// Interrupt control
|
||||
case 0xd:
|
||||
@@ -102,12 +157,13 @@ template <typename T> void MOS6522<T>::set_register(int address, uint8_t value)
|
||||
|
||||
template <typename T> uint8_t MOS6522<T>::get_register(int address) {
|
||||
address &= 0xf;
|
||||
access(address);
|
||||
switch(address) {
|
||||
case 0x0:
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | InterruptFlag::CB2ActiveEdge);
|
||||
reevaluate_interrupts();
|
||||
return get_port_input(Port::B, registers_.data_direction[1], registers_.output[1]);
|
||||
case 0xf: // TODO: handshake, latching
|
||||
case 0xf:
|
||||
case 0x1:
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge);
|
||||
reevaluate_interrupts();
|
||||
@@ -132,7 +188,11 @@ template <typename T> uint8_t MOS6522<T>::get_register(int address) {
|
||||
return registers_.timer[1] & 0x00ff;
|
||||
case 0x9: return registers_.timer[1] >> 8;
|
||||
|
||||
case 0xa: return registers_.shift;
|
||||
case 0xa:
|
||||
shift_bits_remaining_ = 8;
|
||||
registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister;
|
||||
reevaluate_interrupts();
|
||||
return registers_.shift;
|
||||
|
||||
case 0xb: return registers_.auxiliary_control;
|
||||
case 0xc: return registers_.peripheral_control;
|
||||
@@ -145,7 +205,8 @@ template <typename T> uint8_t MOS6522<T>::get_register(int address) {
|
||||
}
|
||||
|
||||
template <typename T> uint8_t MOS6522<T>::get_port_input(Port port, uint8_t output_mask, uint8_t output) {
|
||||
uint8_t input = bus_handler_.get_port_input(port);
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
const uint8_t input = bus_handler_.get_port_input(port);
|
||||
return (input & ~output_mask) | (output & output_mask);
|
||||
}
|
||||
|
||||
@@ -158,9 +219,238 @@ template <typename T> void MOS6522<T>::reevaluate_interrupts() {
|
||||
bool new_interrupt_status = get_interrupt_line();
|
||||
if(new_interrupt_status != last_posted_interrupt_status_) {
|
||||
last_posted_interrupt_status_ = new_interrupt_status;
|
||||
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_interrupt_status(new_interrupt_status);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::set_control_line_input(Port port, Line line, bool value) {
|
||||
switch(line) {
|
||||
case Line::One:
|
||||
if(value != control_inputs_[port].lines[line]) {
|
||||
// In handshake mode, any transition on C[A/B]1 sets output high on C[A/B]2.
|
||||
if(handshake_modes_[port] == HandshakeMode::Handshake) {
|
||||
set_control_line_output(port, Line::Two, LineState::On);
|
||||
}
|
||||
|
||||
// Set the proper transition interrupt bit if enabled.
|
||||
if(value == !!(registers_.peripheral_control & (port ? 0x10 : 0x01))) {
|
||||
registers_.interrupt_flags |= port ? InterruptFlag::CB1ActiveEdge : InterruptFlag::CA1ActiveEdge;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
|
||||
// If this is a transition on CB1, consider updating the shift register.
|
||||
// TODO: and at least one full clock since the shift register was written?
|
||||
if(port == Port::B) {
|
||||
switch(shift_mode()) {
|
||||
default: break;
|
||||
case ShiftMode::InUnderCB1: if(value) shift_in(); break; // Shifts in are captured on a low-to-high transition.
|
||||
case ShiftMode::OutUnderCB1: if(!value) shift_out(); break; // Shifts out are updated on a high-to-low transition.
|
||||
}
|
||||
}
|
||||
}
|
||||
control_inputs_[port].lines[line] = value;
|
||||
break;
|
||||
|
||||
case Line::Two:
|
||||
if( value != control_inputs_[port].lines[line] && // i.e. value has changed ...
|
||||
!(registers_.peripheral_control & (port ? 0x80 : 0x08)) && // ... and line is input ...
|
||||
value == !!(registers_.peripheral_control & (port ? 0x40 : 0x04)) // ... and it's either high or low, as required
|
||||
) {
|
||||
registers_.interrupt_flags |= port ? InterruptFlag::CB2ActiveEdge : InterruptFlag::CA2ActiveEdge;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
control_inputs_[port].lines[line] = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::do_phase2() {
|
||||
++ time_since_bus_handler_call_;
|
||||
|
||||
registers_.last_timer[0] = registers_.timer[0];
|
||||
registers_.last_timer[1] = registers_.timer[1];
|
||||
|
||||
if(registers_.timer_needs_reload) {
|
||||
registers_.timer_needs_reload = false;
|
||||
registers_.timer[0] = registers_.timer_latch[0];
|
||||
} else {
|
||||
registers_.timer[0] --;
|
||||
}
|
||||
|
||||
registers_.timer[1] --;
|
||||
if(registers_.next_timer[0] >= 0) {
|
||||
registers_.timer[0] = static_cast<uint16_t>(registers_.next_timer[0]);
|
||||
registers_.next_timer[0] = -1;
|
||||
}
|
||||
if(registers_.next_timer[1] >= 0) {
|
||||
registers_.timer[1] = static_cast<uint16_t>(registers_.next_timer[1]);
|
||||
registers_.next_timer[1] = -1;
|
||||
}
|
||||
|
||||
// In pulse modes, CA2 and CB2 go high again on the next clock edge.
|
||||
if(handshake_modes_[1] == HandshakeMode::Pulse) {
|
||||
set_control_line_output(Port::B, Line::Two, LineState::On);
|
||||
}
|
||||
if(handshake_modes_[0] == HandshakeMode::Pulse) {
|
||||
set_control_line_output(Port::A, Line::Two, LineState::On);
|
||||
}
|
||||
|
||||
// If the shift register is shifting according to the input clock, do a shift.
|
||||
switch(shift_mode()) {
|
||||
default: break;
|
||||
case ShiftMode::InUnderPhase2: shift_in(); break;
|
||||
case ShiftMode::OutUnderPhase2: shift_out(); break;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::do_phase1() {
|
||||
++ time_since_bus_handler_call_;
|
||||
|
||||
// IRQ is raised on the half cycle after overflow
|
||||
if((registers_.timer[1] == 0xffff) && !registers_.last_timer[1] && timer_is_running_[1]) {
|
||||
timer_is_running_[1] = false;
|
||||
|
||||
// If the shift register is shifting according to this timer, do a shift.
|
||||
// TODO: "shift register is driven by only the low order 8 bits of timer 2"?
|
||||
switch(shift_mode()) {
|
||||
default: break;
|
||||
case ShiftMode::InUnderT2: shift_in(); break;
|
||||
case ShiftMode::OutUnderT2FreeRunning: shift_out(); break;
|
||||
case ShiftMode::OutUnderT2: shift_out(); break; // TODO: present a clock on CB1.
|
||||
}
|
||||
|
||||
registers_.interrupt_flags |= InterruptFlag::Timer2;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
|
||||
if((registers_.timer[0] == 0xffff) && !registers_.last_timer[0] && timer_is_running_[0]) {
|
||||
registers_.interrupt_flags |= InterruptFlag::Timer1;
|
||||
reevaluate_interrupts();
|
||||
|
||||
// Determine whether to reload.
|
||||
if(registers_.auxiliary_control&0x40)
|
||||
registers_.timer_needs_reload = true;
|
||||
else
|
||||
timer_is_running_[0] = false;
|
||||
|
||||
// Determine whether to toggle PB7.
|
||||
if(registers_.auxiliary_control&0x80) {
|
||||
registers_.output[1] ^= 0x80;
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_port_output(Port::B, registers_.output[1], registers_.data_direction[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*! Runs for a specified number of half cycles. */
|
||||
template <typename T> void MOS6522<T>::run_for(const HalfCycles half_cycles) {
|
||||
int number_of_half_cycles = half_cycles.as_int();
|
||||
if(!number_of_half_cycles) return;
|
||||
|
||||
if(is_phase2_) {
|
||||
do_phase2();
|
||||
number_of_half_cycles--;
|
||||
}
|
||||
|
||||
while(number_of_half_cycles >= 2) {
|
||||
do_phase1();
|
||||
do_phase2();
|
||||
number_of_half_cycles -= 2;
|
||||
}
|
||||
|
||||
if(number_of_half_cycles) {
|
||||
do_phase1();
|
||||
is_phase2_ = true;
|
||||
} else {
|
||||
is_phase2_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::flush() {
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.flush();
|
||||
}
|
||||
|
||||
/*! Runs for a specified number of cycles. */
|
||||
template <typename T> void MOS6522<T>::run_for(const Cycles cycles) {
|
||||
int number_of_cycles = cycles.as_int();
|
||||
while(number_of_cycles--) {
|
||||
do_phase1();
|
||||
do_phase2();
|
||||
}
|
||||
}
|
||||
|
||||
/*! @returns @c true if the IRQ line is currently active; @c false otherwise. */
|
||||
template <typename T> bool MOS6522<T>::get_interrupt_line() {
|
||||
uint8_t interrupt_status = registers_.interrupt_flags & registers_.interrupt_enable & 0x7f;
|
||||
return !!interrupt_status;
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::evaluate_cb2_output() {
|
||||
// CB2 is a special case, being both the line the shift register can output to,
|
||||
// and one that can be used as an input or handshaking output according to the
|
||||
// peripheral control register.
|
||||
|
||||
// My guess: other CB2 functions work only if the shift register is disabled (?).
|
||||
if(shift_mode() != ShiftMode::Disabled) {
|
||||
// Shift register is enabled, one way or the other; but announce only output.
|
||||
if(is_shifting_out()) {
|
||||
// Output mode; set the level according to the current top of the shift register.
|
||||
bus_handler_.set_control_line_output(Port::B, Line::Two, !!(registers_.shift & 0x80));
|
||||
} else {
|
||||
// Input mode.
|
||||
bus_handler_.set_control_line_output(Port::B, Line::Two, true);
|
||||
}
|
||||
} else {
|
||||
// Shift register is disabled.
|
||||
bus_handler_.set_control_line_output(Port::B, Line::Two, control_outputs_[1].lines[1] != LineState::Off);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::set_control_line_output(Port port, Line line, LineState value) {
|
||||
if(port == Port::B && line == Line::Two) {
|
||||
control_outputs_[port].lines[line] = value;
|
||||
evaluate_cb2_output();
|
||||
} else {
|
||||
// Do nothing if unchanged.
|
||||
if(value == control_outputs_[port].lines[line]) {
|
||||
return;
|
||||
}
|
||||
|
||||
control_outputs_[port].lines[line] = value;
|
||||
|
||||
if(value != LineState::Input) {
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.set_control_line_output(port, line, value != LineState::Off);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::shift_in() {
|
||||
registers_.shift = uint8_t((registers_.shift << 1) | (control_inputs_[1].lines[1] ? 1 : 0));
|
||||
--shift_bits_remaining_;
|
||||
if(!shift_bits_remaining_) {
|
||||
registers_.interrupt_flags |= InterruptFlag::ShiftRegister;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void MOS6522<T>::shift_out() {
|
||||
// When shifting out, the shift register rotates rather than strictly shifts.
|
||||
// TODO: is that true for all modes?
|
||||
if(shift_mode() == ShiftMode::OutUnderT2FreeRunning || shift_bits_remaining_) {
|
||||
registers_.shift = uint8_t((registers_.shift << 1) | (registers_.shift >> 7));
|
||||
evaluate_cb2_output();
|
||||
|
||||
--shift_bits_remaining_;
|
||||
if(!shift_bits_remaining_) {
|
||||
registers_.interrupt_flags |= InterruptFlag::ShiftRegister;
|
||||
reevaluate_interrupts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class MOS6522Storage {
|
||||
|
||||
// The registers
|
||||
struct Registers {
|
||||
// "A low reset (RES) input clears all R6522 internal registers to logic 0"
|
||||
// "A low reset (RES) input clears all R6522 internal registers to logic 0"
|
||||
uint8_t output[2] = {0, 0};
|
||||
uint8_t input[2] = {0, 0};
|
||||
uint8_t data_direction[2] = {0, 0};
|
||||
@@ -37,14 +37,27 @@ class MOS6522Storage {
|
||||
bool timer_needs_reload = false;
|
||||
} registers_;
|
||||
|
||||
// control state
|
||||
// Control state.
|
||||
struct {
|
||||
bool line_one = false;
|
||||
bool line_two = false;
|
||||
bool lines[2] = {false, false};
|
||||
} control_inputs_[2];
|
||||
|
||||
enum class LineState {
|
||||
On, Off, Input
|
||||
};
|
||||
struct {
|
||||
LineState lines[2] = {LineState::Input, LineState::Input};
|
||||
} control_outputs_[2];
|
||||
|
||||
enum class HandshakeMode {
|
||||
None,
|
||||
Handshake,
|
||||
Pulse
|
||||
} handshake_modes_[2] = { HandshakeMode::None, HandshakeMode::None };
|
||||
|
||||
bool timer_is_running_[2] = {false, false};
|
||||
bool last_posted_interrupt_status_ = false;
|
||||
int shift_bits_remaining_ = 8;
|
||||
|
||||
enum InterruptFlag: uint8_t {
|
||||
CA2ActiveEdge = 1 << 0,
|
||||
@@ -55,6 +68,23 @@ class MOS6522Storage {
|
||||
Timer2 = 1 << 5,
|
||||
Timer1 = 1 << 6,
|
||||
};
|
||||
|
||||
enum class ShiftMode {
|
||||
Disabled = 0,
|
||||
InUnderT2 = 1,
|
||||
InUnderPhase2 = 2,
|
||||
InUnderCB1 = 3,
|
||||
OutUnderT2FreeRunning = 4,
|
||||
OutUnderT2 = 5,
|
||||
OutUnderPhase2 = 6,
|
||||
OutUnderCB1 = 7
|
||||
};
|
||||
ShiftMode shift_mode() const {
|
||||
return ShiftMode((registers_.auxiliary_control >> 2) & 7);
|
||||
}
|
||||
bool is_shifting_out() const {
|
||||
return registers_.auxiliary_control & 0x10;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
268
Components/8530/z8530.cpp
Normal file
268
Components/8530/z8530.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
//
|
||||
// 8530.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "z8530.hpp"
|
||||
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
using namespace Zilog::SCC;
|
||||
|
||||
void z8530::reset() {
|
||||
// TODO.
|
||||
}
|
||||
|
||||
bool z8530::get_interrupt_line() {
|
||||
return
|
||||
(master_interrupt_control_ & 0x8) &&
|
||||
(
|
||||
channels_[0].get_interrupt_line() ||
|
||||
channels_[1].get_interrupt_line()
|
||||
);
|
||||
}
|
||||
|
||||
std::uint8_t z8530::read(int address) {
|
||||
if(address & 2) {
|
||||
// Read data register for channel
|
||||
return 0x00;
|
||||
} else {
|
||||
// Read control register for channel.
|
||||
uint8_t result = 0;
|
||||
|
||||
switch(pointer_) {
|
||||
default:
|
||||
result = channels_[address & 1].read(address & 2, pointer_);
|
||||
break;
|
||||
|
||||
case 2: // Handled non-symmetrically between channels.
|
||||
if(address & 1) {
|
||||
LOG("[SCC] Unimplemented: register 2 status bits");
|
||||
} else {
|
||||
result = interrupt_vector_;
|
||||
|
||||
// Modify the vector if permitted.
|
||||
// if(master_interrupt_control_ & 1) {
|
||||
for(int port = 0; port < 2; ++port) {
|
||||
// TODO: the logic below assumes that DCD is the only implemented interrupt. Fix.
|
||||
if(channels_[port].get_interrupt_line()) {
|
||||
const uint8_t shift = 1 + 3*((master_interrupt_control_ & 0x10) >> 4);
|
||||
const uint8_t mask = uint8_t(~(7 << shift));
|
||||
result = uint8_t(
|
||||
(result & mask) |
|
||||
((1 | ((port == 1) ? 4 : 0)) << shift)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
pointer_ = 0;
|
||||
update_delegate();
|
||||
return result;
|
||||
}
|
||||
|
||||
return 0x00;
|
||||
}
|
||||
|
||||
void z8530::write(int address, std::uint8_t value) {
|
||||
if(address & 2) {
|
||||
// Write data register for channel.
|
||||
} else {
|
||||
// Write control register for channel.
|
||||
|
||||
// Most registers are per channel, but a couple are shared; sever
|
||||
// them here.
|
||||
switch(pointer_) {
|
||||
default:
|
||||
channels_[address & 1].write(address & 2, pointer_, value);
|
||||
break;
|
||||
|
||||
case 2: // Interrupt vector register; shared between both channels.
|
||||
interrupt_vector_ = value;
|
||||
LOG("[SCC] Interrupt vector set to " << PADHEX(2) << int(value));
|
||||
break;
|
||||
|
||||
case 9: // Master interrupt and reset register; also shared between both channels.
|
||||
LOG("[SCC] Master interrupt and reset register: " << PADHEX(2) << int(value));
|
||||
master_interrupt_control_ = value;
|
||||
break;
|
||||
}
|
||||
|
||||
// The pointer number resets to 0 after every access, but if it is zero
|
||||
// then crib at least the next set of pointer bits (which, similarly, are shared
|
||||
// between the two channels).
|
||||
if(pointer_) {
|
||||
pointer_ = 0;
|
||||
} else {
|
||||
// The lowest three bits are the lowest three bits of the pointer.
|
||||
pointer_ = value & 7;
|
||||
|
||||
// If the command part of the byte is a 'point high', also set the
|
||||
// top bit of the pointer.
|
||||
if(((value >> 3)&7) == 1) {
|
||||
pointer_ |= 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
update_delegate();
|
||||
}
|
||||
|
||||
void z8530::set_dcd(int port, bool level) {
|
||||
channels_[port].set_dcd(level);
|
||||
update_delegate();
|
||||
}
|
||||
|
||||
// MARK: - Channel implementations
|
||||
|
||||
uint8_t z8530::Channel::read(bool data, uint8_t pointer) {
|
||||
// If this is a data read, just return it.
|
||||
if(data) {
|
||||
return data_;
|
||||
} else {
|
||||
// Otherwise, this is a control read...
|
||||
switch(pointer) {
|
||||
default:
|
||||
LOG("[SCC] Unrecognised control read from register " << int(pointer));
|
||||
return 0x00;
|
||||
|
||||
case 0:
|
||||
return dcd_ ? 0x8 : 0x0;
|
||||
|
||||
case 0xf:
|
||||
return external_interrupt_status_;
|
||||
}
|
||||
}
|
||||
|
||||
return 0x00;
|
||||
}
|
||||
|
||||
void z8530::Channel::write(bool data, uint8_t pointer, uint8_t value) {
|
||||
if(data) {
|
||||
data_ = value;
|
||||
return;
|
||||
} else {
|
||||
switch(pointer) {
|
||||
default:
|
||||
LOG("[SCC] Unrecognised control write: " << PADHEX(2) << int(value) << " to register " << int(pointer));
|
||||
break;
|
||||
|
||||
case 0x0: // Write register 0 — CRC reset and other functions.
|
||||
// Decode CRC reset instructions.
|
||||
switch(value >> 6) {
|
||||
default: /* Do nothing. */ break;
|
||||
case 1:
|
||||
LOG("[SCC] TODO: reset Rx CRC checker.");
|
||||
break;
|
||||
case 2:
|
||||
LOG("[SCC] TODO: reset Tx CRC checker.");
|
||||
break;
|
||||
case 3:
|
||||
LOG("[SCC] TODO: reset Tx underrun/EOM latch.");
|
||||
break;
|
||||
}
|
||||
|
||||
// Decode command code.
|
||||
switch((value >> 3)&7) {
|
||||
default: /* Do nothing. */ break;
|
||||
case 2:
|
||||
// LOG("[SCC] reset ext/status interrupts.");
|
||||
external_status_interrupt_ = false;
|
||||
external_interrupt_status_ = 0;
|
||||
break;
|
||||
case 3:
|
||||
LOG("[SCC] TODO: send abort (SDLC).");
|
||||
break;
|
||||
case 4:
|
||||
LOG("[SCC] TODO: enable interrupt on next Rx character.");
|
||||
break;
|
||||
case 5:
|
||||
LOG("[SCC] TODO: reset Tx interrupt pending.");
|
||||
break;
|
||||
case 6:
|
||||
LOG("[SCC] TODO: reset error.");
|
||||
break;
|
||||
case 7:
|
||||
LOG("[SCC] TODO: reset highest IUS.");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x1: // Write register 1 — Transmit/Receive Interrupt and Data Transfer Mode Definition.
|
||||
interrupt_mask_ = value;
|
||||
break;
|
||||
|
||||
case 0x4: // Write register 4 — Transmit/Receive Miscellaneous Parameters and Modes.
|
||||
// Bits 0 and 1 select parity mode.
|
||||
if(!(value&1)) {
|
||||
parity_ = Parity::Off;
|
||||
} else {
|
||||
parity_ = (value&2) ? Parity::Even : Parity::Odd;
|
||||
}
|
||||
|
||||
// Bits 2 and 3 select stop bits.
|
||||
switch((value >> 2)&3) {
|
||||
default: stop_bits_ = StopBits::Synchronous; break;
|
||||
case 1: stop_bits_ = StopBits::OneBit; break;
|
||||
case 2: stop_bits_ = StopBits::OneAndAHalfBits; break;
|
||||
case 3: stop_bits_ = StopBits::TwoBits; break;
|
||||
}
|
||||
|
||||
// Bits 4 and 5 pick a sync mode.
|
||||
switch((value >> 4)&3) {
|
||||
default: sync_mode_ = Sync::Monosync; break;
|
||||
case 1: sync_mode_ = Sync::Bisync; break;
|
||||
case 2: sync_mode_ = Sync::SDLC; break;
|
||||
case 3: sync_mode_ = Sync::External; break;
|
||||
}
|
||||
|
||||
// Bits 6 and 7 select a clock rate multiplier, unless synchronous
|
||||
// mode is enabled (and this is ignored if sync mode is external).
|
||||
if(stop_bits_ == StopBits::Synchronous) {
|
||||
clock_rate_multiplier_ = 1;
|
||||
} else {
|
||||
switch((value >> 6)&3) {
|
||||
default: clock_rate_multiplier_ = 1; break;
|
||||
case 1: clock_rate_multiplier_ = 16; break;
|
||||
case 2: clock_rate_multiplier_ = 32; break;
|
||||
case 3: clock_rate_multiplier_ = 64; break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xf: // Write register 15 — External/Status Interrupt Control.
|
||||
external_interrupt_mask_ = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void z8530::Channel::set_dcd(bool level) {
|
||||
if(dcd_ == level) return;
|
||||
dcd_ = level;
|
||||
|
||||
if(external_interrupt_mask_ & 0x8) {
|
||||
external_status_interrupt_ = true;
|
||||
external_interrupt_status_ |= 0x8;
|
||||
}
|
||||
}
|
||||
|
||||
bool z8530::Channel::get_interrupt_line() {
|
||||
return
|
||||
(interrupt_mask_ & 1) && external_status_interrupt_;
|
||||
// TODO: other potential causes of an interrupt.
|
||||
}
|
||||
|
||||
void z8530::update_delegate() {
|
||||
const bool interrupt_line = get_interrupt_line();
|
||||
if(interrupt_line != previous_interrupt_line_) {
|
||||
previous_interrupt_line_ = interrupt_line;
|
||||
if(delegate_) delegate_->did_change_interrupt_status(this, interrupt_line);
|
||||
}
|
||||
}
|
||||
99
Components/8530/z8530.hpp
Normal file
99
Components/8530/z8530.hpp
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// z8530.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef z8530_hpp
|
||||
#define z8530_hpp
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Zilog {
|
||||
namespace SCC {
|
||||
|
||||
/*!
|
||||
Models the Zilog 8530 SCC, a serial adaptor.
|
||||
*/
|
||||
class z8530 {
|
||||
public:
|
||||
/*
|
||||
**Interface for emulated machine.**
|
||||
|
||||
Notes on addressing below:
|
||||
|
||||
There's no inherent ordering of the two 'address' lines,
|
||||
A/B and C/D, but the methods below assume:
|
||||
|
||||
A/B = A0
|
||||
C/D = A1
|
||||
*/
|
||||
std::uint8_t read(int address);
|
||||
void write(int address, std::uint8_t value);
|
||||
void reset();
|
||||
bool get_interrupt_line();
|
||||
|
||||
struct Delegate {
|
||||
virtual void did_change_interrupt_status(z8530 *, bool new_status) = 0;
|
||||
};
|
||||
void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
|
||||
/*
|
||||
**Interface for serial port input.**
|
||||
*/
|
||||
void set_dcd(int port, bool level);
|
||||
|
||||
private:
|
||||
class Channel {
|
||||
public:
|
||||
uint8_t read(bool data, uint8_t pointer);
|
||||
void write(bool data, uint8_t pointer, uint8_t value);
|
||||
void set_dcd(bool level);
|
||||
bool get_interrupt_line();
|
||||
|
||||
private:
|
||||
uint8_t data_ = 0xff;
|
||||
|
||||
enum class Parity {
|
||||
Even, Odd, Off
|
||||
} parity_ = Parity::Off;
|
||||
|
||||
enum class StopBits {
|
||||
Synchronous, OneBit, OneAndAHalfBits, TwoBits
|
||||
} stop_bits_ = StopBits::Synchronous;
|
||||
|
||||
enum class Sync {
|
||||
Monosync, Bisync, SDLC, External
|
||||
} sync_mode_ = Sync::Monosync;
|
||||
|
||||
int clock_rate_multiplier_ = 1;
|
||||
|
||||
uint8_t interrupt_mask_ = 0; // i.e. Write Register 0x1.
|
||||
|
||||
uint8_t external_interrupt_mask_ = 0; // i.e. Write Register 0xf.
|
||||
bool external_status_interrupt_ = false;
|
||||
uint8_t external_interrupt_status_ = 0;
|
||||
|
||||
bool dcd_ = false;
|
||||
} channels_[2];
|
||||
|
||||
uint8_t pointer_ = 0;
|
||||
|
||||
uint8_t interrupt_vector_ = 0;
|
||||
|
||||
uint8_t master_interrupt_control_ = 0;
|
||||
|
||||
bool previous_interrupt_line_ = false;
|
||||
void update_delegate();
|
||||
Delegate *delegate_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif /* z8530_hpp */
|
||||
@@ -173,11 +173,15 @@ void AY38910::select_register(uint8_t r) {
|
||||
}
|
||||
|
||||
void AY38910::set_register_value(uint8_t value) {
|
||||
// There are only 16 registers.
|
||||
if(selected_register_ > 15) return;
|
||||
registers_[selected_register_] = value;
|
||||
|
||||
// If this is a register that affects audio output, enqueue a mutation onto the
|
||||
// audio generation thread.
|
||||
if(selected_register_ < 14) {
|
||||
int selected_register = selected_register_;
|
||||
const int selected_register = selected_register_;
|
||||
task_queue_.defer([=] () {
|
||||
// Perform any register-specific mutation to output generation.
|
||||
uint8_t masked_value = value;
|
||||
switch(selected_register) {
|
||||
case 0: case 2: case 4:
|
||||
@@ -208,12 +212,34 @@ void AY38910::set_register_value(uint8_t value) {
|
||||
envelope_position_ = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Store a copy of the current register within the storage used by the audio generation
|
||||
// thread, and apply any changes to output volume.
|
||||
output_registers_[selected_register] = masked_value;
|
||||
evaluate_output_volume();
|
||||
});
|
||||
} else {
|
||||
if(port_handler_) port_handler_->set_port_output(selected_register_ == 15, value);
|
||||
}
|
||||
|
||||
// Decide which outputs are going to need updating (if any).
|
||||
bool update_port_a = false;
|
||||
bool update_port_b = true;
|
||||
if(port_handler_) {
|
||||
if(selected_register_ == 7) {
|
||||
const uint8_t io_change = registers_[7] ^ value;
|
||||
update_port_b = !!(io_change&0x80);
|
||||
update_port_a = !!(io_change&0x40);
|
||||
} else {
|
||||
update_port_b = selected_register_ == 15;
|
||||
update_port_a = selected_register_ != 15;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep a copy of the new value that is usable from the emulation thread.
|
||||
registers_[selected_register_] = value;
|
||||
|
||||
// Update ports as required.
|
||||
if(update_port_b) set_port_output(true);
|
||||
if(update_port_a) set_port_output(false);
|
||||
}
|
||||
|
||||
uint8_t AY38910::get_register_value() {
|
||||
@@ -238,6 +264,8 @@ uint8_t AY38910::get_port_output(bool port_b) {
|
||||
|
||||
void AY38910::set_port_handler(PortHandler *handler) {
|
||||
port_handler_ = handler;
|
||||
set_port_output(true);
|
||||
set_port_output(false);
|
||||
}
|
||||
|
||||
void AY38910::set_data_input(uint8_t r) {
|
||||
@@ -245,6 +273,16 @@ void AY38910::set_data_input(uint8_t r) {
|
||||
update_bus();
|
||||
}
|
||||
|
||||
void AY38910::set_port_output(bool port_b) {
|
||||
// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor,
|
||||
// so that when in the "input" mode, all pins will read normally high". Therefore,
|
||||
// report programmer selection of input mode as creating an output of 0xff.
|
||||
if(port_handler_) {
|
||||
const bool is_output = !!(registers_[7] & (port_b ? 0x80 : 0x40));
|
||||
port_handler_->set_port_output(port_b, is_output ? registers_[port_b ? 15 : 14] : 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t AY38910::get_data_output() {
|
||||
if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) {
|
||||
// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the
|
||||
|
||||
@@ -83,7 +83,7 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
*/
|
||||
void set_port_handler(PortHandler *);
|
||||
|
||||
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter; not for public consumption
|
||||
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
bool is_zero_level();
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
@@ -128,10 +128,11 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
uint8_t data_input_, data_output_;
|
||||
|
||||
int16_t output_volume_;
|
||||
inline void evaluate_output_volume();
|
||||
void evaluate_output_volume();
|
||||
|
||||
inline void update_bus();
|
||||
void update_bus();
|
||||
PortHandler *port_handler_ = nullptr;
|
||||
void set_port_output(bool port_b);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace {
|
||||
DiskII::DiskII(int clock_rate) :
|
||||
clock_rate_(clock_rate),
|
||||
inputs_(input_command),
|
||||
drives_{{static_cast<unsigned int>(clock_rate), 300, 1}, {static_cast<unsigned int>(clock_rate), 300, 1}}
|
||||
drives_{{clock_rate, 300, 1}, {clock_rate, 300, 1}}
|
||||
{
|
||||
drives_[0].set_clocking_hint_observer(this);
|
||||
drives_[1].set_clocking_hint_observer(this);
|
||||
@@ -211,7 +211,7 @@ void DiskII::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int driv
|
||||
drives_[drive].set_disk(disk);
|
||||
}
|
||||
|
||||
void DiskII::process_event(const Storage::Disk::Track::Event &event) {
|
||||
void DiskII::process_event(const Storage::Disk::Drive::Event &event) {
|
||||
if(event.type == Storage::Disk::Track::Event::FluxTransition) {
|
||||
inputs_ &= ~input_flux;
|
||||
flux_duration_ = 2; // Upon detection of a flux transition, the flux flag should stay set for 1us. Emulate that as two cycles.
|
||||
|
||||
@@ -98,7 +98,7 @@ class DiskII:
|
||||
void select_drive(int drive);
|
||||
|
||||
uint8_t trigger_address(int address, uint8_t value);
|
||||
void process_event(const Storage::Disk::Track::Event &event) override;
|
||||
void process_event(const Storage::Disk::Drive::Event &event) override;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) override;
|
||||
|
||||
const int clock_rate_ = 0;
|
||||
|
||||
376
Components/DiskII/IWM.cpp
Normal file
376
Components/DiskII/IWM.cpp
Normal file
@@ -0,0 +1,376 @@
|
||||
//
|
||||
// IWM.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 05/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "IWM.hpp"
|
||||
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
using namespace Apple;
|
||||
|
||||
namespace {
|
||||
const int CA0 = 1 << 0;
|
||||
const int CA1 = 1 << 1;
|
||||
const int CA2 = 1 << 2;
|
||||
const int LSTRB = 1 << 3;
|
||||
const int ENABLE = 1 << 4;
|
||||
const int DRIVESEL = 1 << 5; /* This means drive select, like on the original Disk II. */
|
||||
const int Q6 = 1 << 6;
|
||||
const int Q7 = 1 << 7;
|
||||
const int SEL = 1 << 8; /* This is an additional input, not available on a Disk II, with a confusingly-similar name to SELECT but a distinct purpose. */
|
||||
}
|
||||
|
||||
IWM::IWM(int clock_rate) :
|
||||
clock_rate_(clock_rate) {}
|
||||
|
||||
// MARK: - Bus accessors
|
||||
|
||||
uint8_t IWM::read(int address) {
|
||||
access(address);
|
||||
|
||||
// Per Inside Macintosh:
|
||||
//
|
||||
// "Before you can read from any of the disk registers you must set up the state of the IWM so that it
|
||||
// can pass the data through to the MC68000's address space where you'll be able to read it. To do that,
|
||||
// you must first turn off Q7 by reading or writing dBase+q7L. Then turn on Q6 by accessing dBase+q6H.
|
||||
// After that, the IWM will be able to pass data from the disk's RD/SENSE line through to you."
|
||||
//
|
||||
// My understanding:
|
||||
//
|
||||
// Q6 = 1, Q7 = 0 reads the status register. The meaning of the top 'SENSE' bit is then determined by
|
||||
// the CA0,1,2 and SEL switches as described in Inside Macintosh, summarised above as RD/SENSE.
|
||||
|
||||
if(address&1) {
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
switch(state_ & (Q6 | Q7 | ENABLE)) {
|
||||
default:
|
||||
LOG("[IWM] Invalid read\n");
|
||||
return 0xff;
|
||||
|
||||
// "Read all 1s".
|
||||
// printf("Reading all 1s\n");
|
||||
// return 0xff;
|
||||
|
||||
case 0:
|
||||
case ENABLE: { /* Read data register. Zeroing afterwards is a guess. */
|
||||
const auto result = data_register_;
|
||||
|
||||
if(data_register_ & 0x80) {
|
||||
// printf("\n\nIWM:%02x\n\n", data_register_);
|
||||
// printf(".");
|
||||
data_register_ = 0;
|
||||
}
|
||||
// LOG("Reading data register: " << PADHEX(2) << int(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case Q6: case Q6|ENABLE: {
|
||||
/*
|
||||
[If A = 0], Read status register:
|
||||
|
||||
bits 0-4: same as mode register.
|
||||
bit 5: 1 = either /ENBL1 or /ENBL2 is currently low.
|
||||
bit 6: 1 = MZ (reserved for future compatibility; should always be read as 0).
|
||||
bit 7: 1 = SENSE input high; 0 = SENSE input low.
|
||||
|
||||
(/ENBL1 is low when the first drive's motor is on; /ENBL2 is low when the second drive's motor is on.
|
||||
If the 1-second timer is enabled, motors remain on for one second after being programmatically disabled.)
|
||||
*/
|
||||
|
||||
return uint8_t(
|
||||
(mode_&0x1f) |
|
||||
((state_ & ENABLE) ? 0x20 : 0x00) |
|
||||
(sense() & 0x80)
|
||||
);
|
||||
} break;
|
||||
|
||||
case Q7: case Q7|ENABLE:
|
||||
/*
|
||||
Read write-handshake register:
|
||||
|
||||
bits 0-5: reserved for future use (currently read as 1).
|
||||
bit 6: 1 = write state (0 = underrun has occurred; 1 = no underrun so far).
|
||||
bit 7: 1 = write data buffer ready for data (1 = ready; 0 = busy).
|
||||
*/
|
||||
// LOG("Reading write handshake: " << PADHEX(2) << (0x3f | write_handshake_));
|
||||
return 0x3f | write_handshake_;
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
void IWM::write(int address, uint8_t input) {
|
||||
access(address);
|
||||
|
||||
switch(state_ & (Q6 | Q7 | ENABLE)) {
|
||||
default: break;
|
||||
|
||||
case Q7|Q6:
|
||||
/*
|
||||
Write mode register:
|
||||
|
||||
bit 0: 1 = latch mode (should be set in asynchronous mode).
|
||||
bit 1: 0 = synchronous handshake protocol; 1 = asynchronous.
|
||||
bit 2: 0 = 1-second on-board timer enable; 1 = timer disable.
|
||||
bit 3: 0 = slow mode; 1 = fast mode.
|
||||
bit 4: 0 = 7Mhz; 1 = 8Mhz (7 or 8 mHz clock descriptor).
|
||||
bit 5: 1 = test mode; 0 = normal operation.
|
||||
bit 6: 1 = MZ-reset.
|
||||
bit 7: reserved for future expansion.
|
||||
*/
|
||||
|
||||
mode_ = input;
|
||||
|
||||
switch(mode_ & 0x18) {
|
||||
case 0x00: bit_length_ = Cycles(24); break; // slow mode, 7Mhz
|
||||
case 0x08: bit_length_ = Cycles(12); break; // fast mode, 7Mhz
|
||||
case 0x10: bit_length_ = Cycles(32); break; // slow mode, 8Mhz
|
||||
case 0x18: bit_length_ = Cycles(16); break; // fast mode, 8Mhz
|
||||
}
|
||||
LOG("IWM mode is now " << PADHEX(2) << int(mode_));
|
||||
break;
|
||||
|
||||
case Q7|Q6|ENABLE: // Write data register.
|
||||
next_output_ = input;
|
||||
write_handshake_ &= ~0x80;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Switch access
|
||||
|
||||
void IWM::access(int address) {
|
||||
// Keep a record of switch state; bits in state_
|
||||
// should correlate with the anonymous namespace constants
|
||||
// defined at the top of this file — CA0, CA1, etc.
|
||||
address &= 0xf;
|
||||
const auto mask = 1 << (address >> 1);
|
||||
const auto old_state = state_;
|
||||
|
||||
if(address & 1) {
|
||||
state_ |= mask;
|
||||
} else {
|
||||
state_ &= ~mask;
|
||||
}
|
||||
|
||||
// React appropriately to ENABLE and DRIVESEL changes, and changes into/out of write mode.
|
||||
if(old_state != state_) {
|
||||
push_drive_state();
|
||||
|
||||
switch(mask) {
|
||||
default: break;
|
||||
|
||||
case ENABLE:
|
||||
if(address & 1) {
|
||||
if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(true);
|
||||
} else {
|
||||
// If the 1-second delay is enabled, set up a timer for that.
|
||||
if(!(mode_ & 4)) {
|
||||
cycles_until_disable_ = Cycles(clock_rate_);
|
||||
} else {
|
||||
if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DRIVESEL: {
|
||||
const int new_drive = address & 1;
|
||||
if(new_drive != active_drive_) {
|
||||
if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(false);
|
||||
active_drive_ = new_drive;
|
||||
if(drives_[active_drive_]) {
|
||||
drives_[active_drive_]->set_enabled(state_ & ENABLE || (cycles_until_disable_ > Cycles(0)));
|
||||
push_drive_state();
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case Q6:
|
||||
case Q7:
|
||||
select_shift_mode();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IWM::set_select(bool enabled) {
|
||||
// Store SEL as an extra state bit.
|
||||
if(enabled) state_ |= SEL;
|
||||
else state_ &= ~SEL;
|
||||
push_drive_state();
|
||||
}
|
||||
|
||||
void IWM::push_drive_state() {
|
||||
if(drives_[active_drive_]) {
|
||||
const uint8_t drive_control_lines =
|
||||
((state_ & CA0) ? IWMDrive::CA0 : 0) |
|
||||
((state_ & CA1) ? IWMDrive::CA1 : 0) |
|
||||
((state_ & CA2) ? IWMDrive::CA2 : 0) |
|
||||
((state_ & SEL) ? IWMDrive::SEL : 0) |
|
||||
((state_ & LSTRB) ? IWMDrive::LSTRB : 0);
|
||||
drives_[active_drive_]->set_control_lines(drive_control_lines);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Active logic
|
||||
|
||||
void IWM::run_for(const Cycles cycles) {
|
||||
// Check for a timeout of the motor-off timer.
|
||||
if(cycles_until_disable_ > Cycles(0)) {
|
||||
cycles_until_disable_ -= cycles;
|
||||
if(cycles_until_disable_ <= Cycles(0)) {
|
||||
cycles_until_disable_ = Cycles(0);
|
||||
if(drives_[active_drive_])
|
||||
drives_[active_drive_]->set_enabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Activity otherwise depends on mode and motor state.
|
||||
int integer_cycles = cycles.as_int();
|
||||
switch(shift_mode_) {
|
||||
case ShiftMode::Reading:
|
||||
if(drive_is_rotating_[active_drive_]) {
|
||||
while(integer_cycles--) {
|
||||
drives_[active_drive_]->run_for(Cycles(1));
|
||||
++cycles_since_shift_;
|
||||
if(cycles_since_shift_ == bit_length_ + Cycles(2)) {
|
||||
propose_shift(0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while(cycles_since_shift_ + integer_cycles >= bit_length_ + Cycles(2)) {
|
||||
propose_shift(0);
|
||||
integer_cycles -= bit_length_.as_int() + 2 - cycles_since_shift_.as_int();
|
||||
}
|
||||
cycles_since_shift_ += Cycles(integer_cycles);
|
||||
}
|
||||
break;
|
||||
|
||||
case ShiftMode::Writing:
|
||||
if(drives_[active_drive_]->is_writing()) {
|
||||
while(cycles_since_shift_ + integer_cycles >= bit_length_) {
|
||||
const auto cycles_until_write = bit_length_ - cycles_since_shift_;
|
||||
drives_[active_drive_]->run_for(cycles_until_write);
|
||||
|
||||
// Output a flux transition if the top bit is set.
|
||||
drives_[active_drive_]->write_bit(shift_register_ & 0x80);
|
||||
shift_register_ <<= 1;
|
||||
|
||||
integer_cycles -= cycles_until_write.as_int();
|
||||
cycles_since_shift_ = Cycles(0);
|
||||
|
||||
--output_bits_remaining_;
|
||||
if(!output_bits_remaining_) {
|
||||
if(!(write_handshake_ & 0x80)) {
|
||||
write_handshake_ |= 0x80;
|
||||
shift_register_ = next_output_;
|
||||
output_bits_remaining_ = 8;
|
||||
// LOG("Next byte: " << PADHEX(2) << int(shift_register_));
|
||||
} else {
|
||||
write_handshake_ &= ~0x40;
|
||||
drives_[active_drive_]->end_writing();
|
||||
// printf("\n");
|
||||
LOG("Overrun; done.");
|
||||
select_shift_mode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cycles_since_shift_ = integer_cycles;
|
||||
if(integer_cycles) {
|
||||
drives_[active_drive_]->run_for(cycles_since_shift_);
|
||||
}
|
||||
} else {
|
||||
drives_[active_drive_]->run_for(cycles);
|
||||
}
|
||||
break;
|
||||
|
||||
case ShiftMode::CheckingWriteProtect:
|
||||
if(integer_cycles < 8) {
|
||||
shift_register_ = (shift_register_ >> integer_cycles) | (sense() & (0xff << (8 - integer_cycles)));
|
||||
} else {
|
||||
shift_register_ = sense();
|
||||
}
|
||||
|
||||
/* Deliberate fallthrough. */
|
||||
default:
|
||||
if(drive_is_rotating_[active_drive_]) drives_[active_drive_]->run_for(cycles);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void IWM::select_shift_mode() {
|
||||
// Don't allow an ongoing write to be interrupted.
|
||||
if(shift_mode_ == ShiftMode::Writing && drives_[active_drive_] && drives_[active_drive_]->is_writing()) return;
|
||||
|
||||
const auto old_shift_mode = shift_mode_;
|
||||
|
||||
switch(state_ & (Q6|Q7)) {
|
||||
default: shift_mode_ = ShiftMode::CheckingWriteProtect; break;
|
||||
case 0: shift_mode_ = ShiftMode::Reading; break;
|
||||
case Q7:
|
||||
// "The IWM is put into the write state by a transition from the write protect sense state to the
|
||||
// write load state".
|
||||
if(shift_mode_ == ShiftMode::CheckingWriteProtect) shift_mode_ = ShiftMode::Writing;
|
||||
break;
|
||||
}
|
||||
|
||||
// If writing mode just began, set the drive into write mode and cue up the first output byte.
|
||||
if(drives_[active_drive_] && old_shift_mode != ShiftMode::Writing && shift_mode_ == ShiftMode::Writing) {
|
||||
drives_[active_drive_]->begin_writing(Storage::Time(1, clock_rate_ / bit_length_.as_int()), false);
|
||||
shift_register_ = next_output_;
|
||||
write_handshake_ |= 0x80 | 0x40;
|
||||
output_bits_remaining_ = 8;
|
||||
LOG("Seeding output with " << PADHEX(2) << shift_register_);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t IWM::sense() {
|
||||
return drives_[active_drive_] ? (drives_[active_drive_]->read() ? 0xff : 0x00) : 0xff;
|
||||
}
|
||||
|
||||
void IWM::process_event(const Storage::Disk::Drive::Event &event) {
|
||||
if(shift_mode_ != ShiftMode::Reading) return;
|
||||
|
||||
switch(event.type) {
|
||||
case Storage::Disk::Track::Event::IndexHole: return;
|
||||
case Storage::Disk::Track::Event::FluxTransition:
|
||||
propose_shift(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void IWM::propose_shift(uint8_t bit) {
|
||||
// TODO: synchronous mode.
|
||||
|
||||
// LOG("Shifting input");
|
||||
shift_register_ = uint8_t((shift_register_ << 1) | bit);
|
||||
if(shift_register_ & 0x80) {
|
||||
data_register_ = shift_register_;
|
||||
shift_register_ = 0;
|
||||
}
|
||||
cycles_since_shift_ = Cycles(0);
|
||||
}
|
||||
|
||||
void IWM::set_drive(int slot, IWMDrive *drive) {
|
||||
drives_[slot] = drive;
|
||||
drive->set_event_delegate(this);
|
||||
drive->set_clocking_hint_observer(this);
|
||||
}
|
||||
|
||||
void IWM::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) {
|
||||
const bool is_rotating = clocking != ClockingHint::Preference::None;
|
||||
|
||||
if(component == static_cast<ClockingHint::Source *>(drives_[0])) {
|
||||
drive_is_rotating_[0] = is_rotating;
|
||||
} else {
|
||||
drive_is_rotating_[1] = is_rotating;
|
||||
}
|
||||
}
|
||||
117
Components/DiskII/IWM.hpp
Normal file
117
Components/DiskII/IWM.hpp
Normal file
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// IWM.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 05/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef IWM_hpp
|
||||
#define IWM_hpp
|
||||
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../ClockReceiver/ClockingHintSource.hpp"
|
||||
#include "../../Storage/Disk/Drive.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Apple {
|
||||
|
||||
/*!
|
||||
Defines the drive interface used by the IWM, derived from the external pinout as
|
||||
per e.g. https://old.pinouts.ru/HD/MacExtDrive_pinout.shtml
|
||||
|
||||
These are subclassed of Storage::Disk::Drive, so accept any disk the emulator supports,
|
||||
and provide the usual read/write interface for on-disk data.
|
||||
*/
|
||||
struct IWMDrive: public Storage::Disk::Drive {
|
||||
IWMDrive(int input_clock_rate, int number_of_heads) : Storage::Disk::Drive(input_clock_rate, number_of_heads) {}
|
||||
|
||||
enum Line: int {
|
||||
CA0 = 1 << 0,
|
||||
CA1 = 1 << 1,
|
||||
CA2 = 1 << 2,
|
||||
LSTRB = 1 << 3,
|
||||
SEL = 1 << 4,
|
||||
};
|
||||
|
||||
virtual void set_enabled(bool) = 0;
|
||||
virtual void set_control_lines(int) = 0;
|
||||
virtual bool read() = 0;
|
||||
};
|
||||
|
||||
class IWM:
|
||||
public Storage::Disk::Drive::EventDelegate,
|
||||
public ClockingHint::Observer {
|
||||
public:
|
||||
IWM(int clock_rate);
|
||||
|
||||
/// Sets the current external value of the data bus.
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*!
|
||||
Submits an access to address @c address.
|
||||
|
||||
@returns The 8-bit value loaded to the data bus by the IWM.
|
||||
*/
|
||||
uint8_t read(int address);
|
||||
|
||||
/*!
|
||||
Sets the current input of the IWM's SEL line.
|
||||
*/
|
||||
void set_select(bool enabled);
|
||||
|
||||
/// Advances the controller by @c cycles.
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
/// Connects a drive to the IWM.
|
||||
void set_drive(int slot, IWMDrive *drive);
|
||||
|
||||
private:
|
||||
// Storage::Disk::Drive::EventDelegate.
|
||||
void process_event(const Storage::Disk::Drive::Event &event) override;
|
||||
|
||||
const int clock_rate_;
|
||||
|
||||
uint8_t data_register_ = 0;
|
||||
uint8_t mode_ = 0;
|
||||
bool read_write_ready_ = true;
|
||||
bool write_overran_ = false;
|
||||
|
||||
int state_ = 0;
|
||||
|
||||
int active_drive_ = 0;
|
||||
IWMDrive *drives_[2] = {nullptr, nullptr};
|
||||
bool drive_is_rotating_[2] = {false, false};
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override;
|
||||
|
||||
Cycles cycles_until_disable_;
|
||||
uint8_t write_handshake_ = 0x80;
|
||||
|
||||
void access(int address);
|
||||
|
||||
uint8_t shift_register_ = 0;
|
||||
uint8_t next_output_ = 0;
|
||||
int output_bits_remaining_ = 0;
|
||||
|
||||
void propose_shift(uint8_t bit);
|
||||
Cycles cycles_since_shift_;
|
||||
Cycles bit_length_ = Cycles(16);
|
||||
|
||||
void push_drive_state();
|
||||
|
||||
enum class ShiftMode {
|
||||
Reading,
|
||||
Writing,
|
||||
CheckingWriteProtect
|
||||
} shift_mode_;
|
||||
|
||||
uint8_t sense();
|
||||
void select_shift_mode();
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endif /* IWM_hpp */
|
||||
179
Components/DiskII/MacintoshDoubleDensityDrive.cpp
Normal file
179
Components/DiskII/MacintoshDoubleDensityDrive.cpp
Normal file
@@ -0,0 +1,179 @@
|
||||
//
|
||||
// MacintoshDoubleDensityDrive.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 10/07/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "MacintoshDoubleDensityDrive.hpp"
|
||||
|
||||
/*
|
||||
Sources used pervasively:
|
||||
|
||||
http://members.iinet.net.au/~kalandi/apple/AUG/1991/11%20NOV.DEC/DISK.STUFF.html
|
||||
Apple Guide to the Macintosh Family Hardware
|
||||
Inside Macintosh III
|
||||
*/
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
DoubleDensityDrive::DoubleDensityDrive(int input_clock_rate, bool is_800k) :
|
||||
IWMDrive(input_clock_rate, is_800k ? 2 : 1), // Only 800kb drives are double sided.
|
||||
is_800k_(is_800k) {
|
||||
// Start with a valid rotation speed.
|
||||
if(is_800k) {
|
||||
Drive::set_rotation_speed(393.3807f);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Speed Selection
|
||||
|
||||
void DoubleDensityDrive::did_step(Storage::Disk::HeadPosition to_position) {
|
||||
// printf("At track %d\n", to_position.as_int());
|
||||
// The 800kb drive automatically selects rotation speed as a function of
|
||||
// head position; the 400kb drive doesn't do so.
|
||||
if(is_800k_) {
|
||||
/*
|
||||
Numbers below cribbed from the Kryoflux forums; specifically:
|
||||
https://forum.kryoflux.com/viewtopic.php?t=1090
|
||||
|
||||
They can almost be worked out algorithmically, since the point is to
|
||||
produce an almost-constant value for speed*(number of sectors), and:
|
||||
|
||||
393.3807 * 12 = 4720.5684
|
||||
429.1723 * 11 = 4720.895421
|
||||
472.1435 * 10 = 4721.435
|
||||
524.5672 * 9 = 4721.1048
|
||||
590.1098 * 8 = 4720.8784
|
||||
|
||||
So 4721 / (number of sectors per track in zone) would give essentially
|
||||
the same results.
|
||||
*/
|
||||
const int zone = to_position.as_int() >> 4;
|
||||
switch(zone) {
|
||||
case 0: Drive::set_rotation_speed(393.3807f); break;
|
||||
case 1: Drive::set_rotation_speed(429.1723f); break;
|
||||
case 2: Drive::set_rotation_speed(472.1435f); break;
|
||||
case 3: Drive::set_rotation_speed(524.5672f); break;
|
||||
default: Drive::set_rotation_speed(590.1098f); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DoubleDensityDrive::set_rotation_speed(float revolutions_per_minute) {
|
||||
if(!is_800k_) {
|
||||
// Don't allow drive speeds to drop below 10 RPM, as a temporary sop
|
||||
// to sanity.
|
||||
Drive::set_rotation_speed(std::max(10.0f, revolutions_per_minute));
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Control input/output.
|
||||
|
||||
void DoubleDensityDrive::set_enabled(bool) {
|
||||
}
|
||||
|
||||
void DoubleDensityDrive::set_control_lines(int lines) {
|
||||
const auto old_state = control_state_;
|
||||
control_state_ = lines;
|
||||
|
||||
// Catch low-to-high LSTRB transitions.
|
||||
if((old_state ^ control_state_) & control_state_ & Line::LSTRB) {
|
||||
switch(control_state_ & (Line::CA2 | Line::CA1 | Line::CA0 | Line::SEL)) {
|
||||
default:
|
||||
break;
|
||||
|
||||
case 0: // Set step direction — CA2 set => step outward.
|
||||
case Line::CA2:
|
||||
step_direction_ = (control_state_ & Line::CA2) ? -1 : 1;
|
||||
break;
|
||||
|
||||
case Line::CA1: // Set drive motor — CA2 set => motor off.
|
||||
case Line::CA1|Line::CA2:
|
||||
set_motor_on(!(control_state_ & Line::CA2));
|
||||
break;
|
||||
|
||||
case Line::CA0: // Initiate a step.
|
||||
step(Storage::Disk::HeadPosition(step_direction_));
|
||||
break;
|
||||
|
||||
case Line::SEL|Line::CA2: // Reset new disk flag.
|
||||
has_new_disk_ = false;
|
||||
break;
|
||||
|
||||
case Line::CA2 | Line::CA1 | Line::CA0: // Eject the disk.
|
||||
set_disk(nullptr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DoubleDensityDrive::read() {
|
||||
switch(control_state_ & (CA2 | CA1 | CA0 | SEL)) {
|
||||
default:
|
||||
return false;
|
||||
|
||||
case 0: // Head step direction.
|
||||
// (0 = inward)
|
||||
return step_direction_ <= 0;
|
||||
|
||||
case SEL: // Disk in place.
|
||||
// (0 = disk present)
|
||||
return !has_disk();
|
||||
|
||||
case CA0: // Disk head step completed.
|
||||
// (0 = still stepping)
|
||||
return true; // TODO: stepping delay. But at the main Drive level.
|
||||
|
||||
case CA0|SEL: // Disk locked.
|
||||
// (0 = write protected)
|
||||
return !get_is_read_only();
|
||||
|
||||
case CA1: // Disk motor running.
|
||||
// (0 = motor on)
|
||||
return !get_motor_on();
|
||||
|
||||
case CA1|SEL: // Head at track 0.
|
||||
// (0 = at track 0)
|
||||
// "This bit becomes valid beginning 12 msec after the step that places the head at track 0."
|
||||
return !get_is_track_zero();
|
||||
|
||||
case CA1|CA0: // Disk has been ejected.
|
||||
// (0 = user has ejected disk)
|
||||
return !has_new_disk_;
|
||||
|
||||
case CA1|CA0|SEL: // Tachometer.
|
||||
// (arbitrary)
|
||||
return get_tachometer();
|
||||
|
||||
case CA2: // Read data, lower head.
|
||||
set_head(0);
|
||||
return false;
|
||||
|
||||
case CA2|SEL: // Read data, upper head.
|
||||
set_head(1);
|
||||
return false;
|
||||
|
||||
case CA2|CA1: // Single- or double-sided drive.
|
||||
// (0 = single sided)
|
||||
return get_head_count() != 1;
|
||||
|
||||
case CA2|CA1|CA0: // "Present/HD" (per the Mac Plus ROM)
|
||||
// (0 = ??HD??)
|
||||
//
|
||||
// Alternative explanation: "Disk ready for reading?"
|
||||
// (0 = ready)
|
||||
return false;
|
||||
|
||||
case CA2|CA1|CA0|SEL: // Drive installed.
|
||||
// (0 = present, 1 = missing)
|
||||
//
|
||||
// TODO: why do I need to return this the wrong way around for the Mac Plus?
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void DoubleDensityDrive::did_set_disk() {
|
||||
has_new_disk_ = true;
|
||||
}
|
||||
53
Components/DiskII/MacintoshDoubleDensityDrive.hpp
Normal file
53
Components/DiskII/MacintoshDoubleDensityDrive.hpp
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// MacintoshDoubleDensityDrive.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 10/07/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef MacintoshDoubleDensityDrive_hpp
|
||||
#define MacintoshDoubleDensityDrive_hpp
|
||||
|
||||
#include "IWM.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
class DoubleDensityDrive: public IWMDrive {
|
||||
public:
|
||||
DoubleDensityDrive(int input_clock_rate, bool is_800k);
|
||||
|
||||
/*!
|
||||
@returns @c true if this is an 800kb drive; @c false otherwise.
|
||||
*/
|
||||
bool is_800k() {
|
||||
return is_800k_;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current rotation speed of this drive only if it is a 400kb drive.
|
||||
800kb drives select their own rotation speed based on head position,
|
||||
and ignore this input.
|
||||
*/
|
||||
void set_rotation_speed(float revolutions_per_minute);
|
||||
|
||||
void set_enabled(bool) override;
|
||||
void set_control_lines(int) override;
|
||||
bool read() override;
|
||||
|
||||
private:
|
||||
// To receive the proper notifications from Storage::Disk::Drive.
|
||||
void did_step(Storage::Disk::HeadPosition to_position) override;
|
||||
void did_set_disk() override;
|
||||
|
||||
const bool is_800k_;
|
||||
bool has_new_disk_ = false;
|
||||
int control_state_ = 0;
|
||||
int step_direction_ = 1;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* MacintoshDoubleDensityDrive_hpp */
|
||||
@@ -6,8 +6,8 @@
|
||||
// Copyright 2017 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Keyboard_hpp
|
||||
#define Keyboard_hpp
|
||||
#ifndef Inputs_Keyboard_hpp
|
||||
#define Inputs_Keyboard_hpp
|
||||
|
||||
#include <vector>
|
||||
#include <set>
|
||||
@@ -75,4 +75,4 @@ class Keyboard {
|
||||
|
||||
}
|
||||
|
||||
#endif /* Keyboard_hpp */
|
||||
#endif /* Inputs_Keyboard_hpp */
|
||||
|
||||
47
Inputs/Mouse.hpp
Normal file
47
Inputs/Mouse.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Mouse.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 11/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Mouse_h
|
||||
#define Mouse_h
|
||||
|
||||
namespace Inputs {
|
||||
|
||||
/*!
|
||||
Models a classic-era mouse: something that provides 2d relative motion plus
|
||||
some quantity of buttons.
|
||||
*/
|
||||
class Mouse {
|
||||
public:
|
||||
/*!
|
||||
Indicates a movement of the mouse.
|
||||
*/
|
||||
virtual void move(int x, int y) {}
|
||||
|
||||
/*!
|
||||
@returns the number of buttons on this mouse.
|
||||
*/
|
||||
virtual int get_number_of_buttons() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*!
|
||||
Indicates that button @c index is now either pressed or unpressed.
|
||||
The intention is that @c index be semantic, not positional:
|
||||
0 for the primary button, 1 for the secondary, 2 for the tertiary, etc.
|
||||
*/
|
||||
virtual void set_button_pressed(int index, bool is_pressed) {}
|
||||
|
||||
/*!
|
||||
Releases all depressed buttons.
|
||||
*/
|
||||
virtual void reset_all_buttons() {}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* Mouse_h */
|
||||
123
Inputs/QuadratureMouse/QuadratureMouse.hpp
Normal file
123
Inputs/QuadratureMouse/QuadratureMouse.hpp
Normal file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// QuadratureMouse.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 11/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef QuadratureMouse_hpp
|
||||
#define QuadratureMouse_hpp
|
||||
|
||||
#include "../Mouse.hpp"
|
||||
#include <atomic>
|
||||
|
||||
namespace Inputs {
|
||||
|
||||
/*!
|
||||
Provides a simple implementation of a Mouse, designed for simple
|
||||
thread-safe feeding to a machine that accepts quadrature-encoded input.
|
||||
|
||||
TEMPORARY SIMPLIFICATION: it is assumed that the caller will be interested
|
||||
in observing a signal that dictates velocity, sampling the other to
|
||||
obtain direction only on transitions in the velocity signal.
|
||||
|
||||
Or, more concretely, of the two channels per axis, one is accurate only when
|
||||
the other transitions. Hence the discussion of 'primary' and 'secondary'
|
||||
channels below. This is intended to be fixed.
|
||||
*/
|
||||
class QuadratureMouse: public Mouse {
|
||||
public:
|
||||
QuadratureMouse(int number_of_buttons) :
|
||||
number_of_buttons_(number_of_buttons) {}
|
||||
|
||||
/*
|
||||
Inputs, to satisfy the Mouse interface.
|
||||
*/
|
||||
void move(int x, int y) override {
|
||||
// Accumulate all provided motion.
|
||||
axes_[0] += x;
|
||||
axes_[1] += y;
|
||||
}
|
||||
|
||||
int get_number_of_buttons() override {
|
||||
return number_of_buttons_;
|
||||
}
|
||||
|
||||
void set_button_pressed(int index, bool is_pressed) override {
|
||||
if(is_pressed)
|
||||
button_flags_ |= (1 << index);
|
||||
else
|
||||
button_flags_ &= ~(1 << index);
|
||||
}
|
||||
|
||||
void reset_all_buttons() override {
|
||||
button_flags_ = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Outputs.
|
||||
*/
|
||||
|
||||
/*!
|
||||
Applies a single step from the current accumulated mouse movement, which
|
||||
might involve the mouse moving right, or left, or not at all.
|
||||
*/
|
||||
void prepare_step() {
|
||||
for(int axis = 0; axis < 2; ++axis) {
|
||||
// Do nothing if there's no motion to communicate.
|
||||
const int axis_value = axes_[axis];
|
||||
if(!axis_value) continue;
|
||||
|
||||
// Toggle the primary channel and set the secondary for
|
||||
// negative motion. At present the y axis signals the
|
||||
// secondary channel the opposite way around from the
|
||||
// primary.
|
||||
primaries_[axis] ^= 1;
|
||||
secondaries_[axis] = primaries_[axis] ^ axis;
|
||||
if(axis_value > 0) {
|
||||
-- axes_[axis];
|
||||
secondaries_[axis] ^= 1; // Switch to positive motion.
|
||||
} else {
|
||||
++ axes_[axis];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns the two quadrature channels — bit 0 is the 'primary' channel
|
||||
(i.e. the one that can be monitored to observe velocity) and
|
||||
bit 1 is the 'secondary' (i.e. that which can be queried to
|
||||
observe direction).
|
||||
*/
|
||||
int get_channel(int axis) {
|
||||
return primaries_[axis] | (secondaries_[axis] << 1);
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns a bit mask of the currently pressed buttons.
|
||||
*/
|
||||
int get_button_mask() {
|
||||
return button_flags_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns @c true if any mouse motion is waiting to be communicated;
|
||||
@c false otherwise.
|
||||
*/
|
||||
bool has_steps() {
|
||||
return axes_[0] || axes_[1];
|
||||
}
|
||||
|
||||
private:
|
||||
int number_of_buttons_ = 0;
|
||||
std::atomic<int> button_flags_;
|
||||
std::atomic<int> axes_[2];
|
||||
|
||||
int primaries_[2] = {0, 0};
|
||||
int secondaries_[2] = {0, 0};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* QuadratureMouse_hpp */
|
||||
@@ -781,27 +781,37 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
ay_.ay().set_port_handler(&key_state_);
|
||||
|
||||
// construct the list of necessary ROMs
|
||||
std::vector<std::string> required_roms = {"amsdos.rom"};
|
||||
const std::string machine_name = "AmstradCPC";
|
||||
std::vector<ROMMachine::ROM> required_roms = {
|
||||
ROMMachine::ROM(machine_name, "the Amstrad Disk Operating System", "amsdos.rom", 16*1024, 0x1fe22ecd)
|
||||
};
|
||||
std::string model_number;
|
||||
uint32_t crcs[2];
|
||||
switch(target.model) {
|
||||
default:
|
||||
model_number = "6128";
|
||||
has_128k_ = true;
|
||||
crcs[0] = 0x0219bb74;
|
||||
crcs[1] = 0xca6af63d;
|
||||
break;
|
||||
case Analyser::Static::AmstradCPC::Target::Model::CPC464:
|
||||
model_number = "464";
|
||||
has_128k_ = false;
|
||||
crcs[0] = 0x815752df;
|
||||
crcs[1] = 0x7d9a3bac;
|
||||
break;
|
||||
case Analyser::Static::AmstradCPC::Target::Model::CPC664:
|
||||
model_number = "664";
|
||||
has_128k_ = false;
|
||||
crcs[0] = 0x3f5a6dc4;
|
||||
crcs[1] = 0x32fee492;
|
||||
break;
|
||||
}
|
||||
required_roms.push_back("os" + model_number + ".rom");
|
||||
required_roms.push_back("basic" + model_number + ".rom");
|
||||
required_roms.emplace_back(machine_name, "the CPC " + model_number + " firmware", "os" + model_number + ".rom", 16*1024, crcs[0]);
|
||||
required_roms.emplace_back(machine_name, "the CPC " + model_number + " BASIC ROM", "basic" + model_number + ".rom", 16*1024, crcs[1]);
|
||||
|
||||
// fetch and verify the ROMs
|
||||
const auto roms = rom_fetcher("AmstradCPC", required_roms);
|
||||
const auto roms = rom_fetcher(required_roms);
|
||||
|
||||
for(std::size_t index = 0; index < roms.size(); ++index) {
|
||||
auto &data = roms[index];
|
||||
|
||||
@@ -8,33 +8,34 @@
|
||||
|
||||
#include "AppleII.hpp"
|
||||
|
||||
#include "../../Activity/Source.hpp"
|
||||
#include "../MediaTarget.hpp"
|
||||
#include "../CRTMachine.hpp"
|
||||
#include "../JoystickMachine.hpp"
|
||||
#include "../KeyboardMachine.hpp"
|
||||
#include "../Utility/MemoryFuzzer.hpp"
|
||||
#include "../Utility/StringSerialiser.hpp"
|
||||
#include "../../../Activity/Source.hpp"
|
||||
#include "../../MediaTarget.hpp"
|
||||
#include "../../CRTMachine.hpp"
|
||||
#include "../../JoystickMachine.hpp"
|
||||
#include "../../KeyboardMachine.hpp"
|
||||
#include "../../Utility/MemoryFuzzer.hpp"
|
||||
#include "../../Utility/StringSerialiser.hpp"
|
||||
|
||||
#include "../../Processors/6502/6502.hpp"
|
||||
#include "../../Components/AudioToggle/AudioToggle.hpp"
|
||||
#include "../../../Processors/6502/6502.hpp"
|
||||
#include "../../../Components/AudioToggle/AudioToggle.hpp"
|
||||
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../Outputs/Log.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include "Card.hpp"
|
||||
#include "DiskIICard.hpp"
|
||||
#include "Video.hpp"
|
||||
|
||||
#include "../../Analyser/Static/AppleII/Target.hpp"
|
||||
#include "../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../Configurable/StandardOptions.hpp"
|
||||
#include "../../../Analyser/Static/AppleII/Target.hpp"
|
||||
#include "../../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../../Configurable/StandardOptions.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
namespace AppleII {
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
return Configurable::standard_options(
|
||||
@@ -51,12 +52,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public Inputs::Keyboard,
|
||||
public Configurable::Device,
|
||||
public AppleII::Machine,
|
||||
public Apple::II::Machine,
|
||||
public Activity::Source,
|
||||
public JoystickMachine::Machine,
|
||||
public AppleII::Card::Delegate {
|
||||
public Apple::II::Card::Delegate {
|
||||
private:
|
||||
struct VideoBusHandler : public AppleII::Video::BusHandler {
|
||||
struct VideoBusHandler : public Apple::II::Video::BusHandler {
|
||||
public:
|
||||
VideoBusHandler(uint8_t *ram, uint8_t *aux_ram) : ram_(ram), aux_ram_(aux_ram) {}
|
||||
|
||||
@@ -71,12 +72,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
|
||||
CPU::MOS6502::Processor<(model == Analyser::Static::AppleII::Target::Model::EnhancedIIe) ? CPU::MOS6502::Personality::PSynertek65C02 : CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
|
||||
VideoBusHandler video_bus_handler_;
|
||||
AppleII::Video::Video<VideoBusHandler, is_iie()> video_;
|
||||
Apple::II::Video::Video<VideoBusHandler, is_iie()> video_;
|
||||
int cycles_into_current_line_ = 0;
|
||||
Cycles cycles_since_video_update_;
|
||||
|
||||
void update_video() {
|
||||
video_.run_for(cycles_since_video_update_.flush());
|
||||
video_.run_for(cycles_since_video_update_.flush<Cycles>());
|
||||
}
|
||||
static const int audio_divider = 8;
|
||||
void update_audio() {
|
||||
@@ -109,28 +110,28 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
Cycles cycles_since_audio_update_;
|
||||
|
||||
// MARK: - Cards
|
||||
std::array<std::unique_ptr<AppleII::Card>, 7> cards_;
|
||||
std::array<std::unique_ptr<Apple::II::Card>, 7> cards_;
|
||||
Cycles cycles_since_card_update_;
|
||||
std::vector<AppleII::Card *> every_cycle_cards_;
|
||||
std::vector<AppleII::Card *> just_in_time_cards_;
|
||||
std::vector<Apple::II::Card *> every_cycle_cards_;
|
||||
std::vector<Apple::II::Card *> just_in_time_cards_;
|
||||
|
||||
int stretched_cycles_since_card_update_ = 0;
|
||||
|
||||
void install_card(std::size_t slot, AppleII::Card *card) {
|
||||
void install_card(std::size_t slot, Apple::II::Card *card) {
|
||||
assert(slot >= 1 && slot < 8);
|
||||
cards_[slot - 1].reset(card);
|
||||
card->set_delegate(this);
|
||||
pick_card_messaging_group(card);
|
||||
}
|
||||
|
||||
bool is_every_cycle_card(AppleII::Card *card) {
|
||||
bool is_every_cycle_card(Apple::II::Card *card) {
|
||||
return !card->get_select_constraints();
|
||||
}
|
||||
|
||||
void pick_card_messaging_group(AppleII::Card *card) {
|
||||
void pick_card_messaging_group(Apple::II::Card *card) {
|
||||
const bool is_every_cycle = is_every_cycle_card(card);
|
||||
std::vector<AppleII::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_cards_;
|
||||
std::vector<AppleII::Card *> &undesired = is_every_cycle ? just_in_time_cards_ : every_cycle_cards_;
|
||||
std::vector<Apple::II::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_cards_;
|
||||
std::vector<Apple::II::Card *> &undesired = is_every_cycle ? just_in_time_cards_ : every_cycle_cards_;
|
||||
|
||||
if(std::find(intended.begin(), intended.end(), card) != intended.end()) return;
|
||||
auto old_membership = std::find(undesired.begin(), undesired.end(), card);
|
||||
@@ -138,12 +139,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
intended.push_back(card);
|
||||
}
|
||||
|
||||
void card_did_change_select_constraints(AppleII::Card *card) override {
|
||||
void card_did_change_select_constraints(Apple::II::Card *card) override {
|
||||
pick_card_messaging_group(card);
|
||||
}
|
||||
|
||||
AppleII::DiskIICard *diskii_card() {
|
||||
return dynamic_cast<AppleII::DiskIICard *>(cards_[5].get());
|
||||
Apple::II::DiskIICard *diskii_card() {
|
||||
return dynamic_cast<Apple::II::DiskIICard *>(cards_[5].get());
|
||||
}
|
||||
|
||||
// MARK: - Memory Map.
|
||||
@@ -346,30 +347,39 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
|
||||
// Pick the required ROMs.
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
std::vector<std::string> rom_names;
|
||||
const std::string machine_name = "AppleII";
|
||||
std::vector<ROMMachine::ROM> rom_descriptions;
|
||||
size_t rom_size = 12*1024;
|
||||
switch(target.model) {
|
||||
default:
|
||||
rom_names.push_back("apple2-character.rom");
|
||||
rom_names.push_back("apple2o.rom");
|
||||
rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
|
||||
rom_descriptions.emplace_back(machine_name, "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588);
|
||||
break;
|
||||
case Target::Model::IIplus:
|
||||
rom_names.push_back("apple2-character.rom");
|
||||
rom_names.push_back("apple2.rom");
|
||||
rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26);
|
||||
break;
|
||||
case Target::Model::IIe:
|
||||
rom_size += 3840;
|
||||
rom_names.push_back("apple2eu-character.rom");
|
||||
rom_names.push_back("apple2eu.rom");
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1);
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18d);
|
||||
break;
|
||||
case Target::Model::EnhancedIIe:
|
||||
rom_size += 3840;
|
||||
rom_names.push_back("apple2e-character.rom");
|
||||
rom_names.push_back("apple2e.rom");
|
||||
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d);
|
||||
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942);
|
||||
break;
|
||||
}
|
||||
const auto roms = rom_fetcher("AppleII", rom_names);
|
||||
const auto roms = rom_fetcher(rom_descriptions);
|
||||
|
||||
// Try to install a Disk II card now, before checking the ROM list,
|
||||
// to make sure that Disk II dependencies have been communicated.
|
||||
if(target.disk_controller != Target::DiskController::None) {
|
||||
// Apple recommended slot 6 for the (first) Disk II.
|
||||
install_card(6, new Apple::II::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector));
|
||||
}
|
||||
|
||||
// Now, check and move the ROMs.
|
||||
if(!roms[0] || !roms[1]) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
@@ -381,11 +391,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
|
||||
video_.set_character_rom(*roms[0]);
|
||||
|
||||
if(target.disk_controller != Target::DiskController::None) {
|
||||
// Apple recommended slot 6 for the (first) Disk II.
|
||||
install_card(6, new AppleII::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector));
|
||||
}
|
||||
|
||||
// Set up the default memory blocks. On a II or II+ these values will never change.
|
||||
// On a IIe they'll be affected by selection of auxiliary RAM.
|
||||
set_main_paging();
|
||||
@@ -700,7 +705,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
// If this is a card access, figure out which card is at play before determining
|
||||
// the totality of who needs messaging.
|
||||
size_t card_number = 0;
|
||||
AppleII::Card::Select select = AppleII::Card::None;
|
||||
Apple::II::Card::Select select = Apple::II::Card::None;
|
||||
|
||||
if(address >= 0xc100) {
|
||||
/*
|
||||
@@ -708,20 +713,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
0xCn00 to 0xCnff: card n.
|
||||
*/
|
||||
card_number = (address - 0xc100) >> 8;
|
||||
select = AppleII::Card::Device;
|
||||
select = Apple::II::Card::Device;
|
||||
} else {
|
||||
/*
|
||||
Decode the area conventionally used by cards for registers:
|
||||
C0n0 to C0nF: card n - 8.
|
||||
*/
|
||||
card_number = (address - 0xc090) >> 4;
|
||||
select = AppleII::Card::IO;
|
||||
select = Apple::II::Card::IO;
|
||||
}
|
||||
|
||||
// If the selected card is a just-in-time card, update the just-in-time cards,
|
||||
// and then message it specifically.
|
||||
const bool is_read = isReadOperation(operation);
|
||||
AppleII::Card *const target = cards_[static_cast<size_t>(card_number)].get();
|
||||
Apple::II::Card *const target = cards_[static_cast<size_t>(card_number)].get();
|
||||
if(target && !is_every_cycle_card(target)) {
|
||||
update_just_in_time_cards();
|
||||
target->perform_bus_operation(select, is_read, address, value);
|
||||
@@ -732,7 +737,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
for(const auto &card: every_cycle_cards_) {
|
||||
card->run_for(Cycles(1), is_stretched_cycle);
|
||||
card->perform_bus_operation(
|
||||
(card == target) ? select : AppleII::Card::None,
|
||||
(card == target) ? select : Apple::II::Card::None,
|
||||
is_read, address, value);
|
||||
}
|
||||
has_updated_cards = true;
|
||||
@@ -744,7 +749,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
const bool is_read = isReadOperation(operation);
|
||||
for(const auto &card: every_cycle_cards_) {
|
||||
card->run_for(Cycles(1), is_stretched_cycle);
|
||||
card->perform_bus_operation(AppleII::Card::None, is_read, address, value);
|
||||
card->perform_bus_operation(Apple::II::Card::None, is_read, address, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,7 +823,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
|
||||
// MARK:: Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
return AppleII::get_options();
|
||||
return Apple::II::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
@@ -860,9 +865,10 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
using namespace AppleII;
|
||||
using namespace Apple::II;
|
||||
|
||||
Machine *Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
@@ -9,14 +9,15 @@
|
||||
#ifndef AppleII_hpp
|
||||
#define AppleII_hpp
|
||||
|
||||
#include "../../Configurable/Configurable.hpp"
|
||||
#include "../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../ROMMachine.hpp"
|
||||
#include "../../../Configurable/Configurable.hpp"
|
||||
#include "../../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../../ROMMachine.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace AppleII {
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
/// @returns The options available for an Apple II.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options();
|
||||
@@ -29,6 +30,7 @@ class Machine {
|
||||
static Machine *AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* AppleII_hpp */
|
||||
@@ -9,11 +9,12 @@
|
||||
#ifndef Card_h
|
||||
#define Card_h
|
||||
|
||||
#include "../../Processors/6502/6502.hpp"
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../Activity/Observer.hpp"
|
||||
#include "../../../Processors/6502/6502.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../Activity/Observer.hpp"
|
||||
|
||||
namespace AppleII {
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
/*!
|
||||
This provides a small subset of the interface offered to cards installed in
|
||||
@@ -109,6 +110,7 @@ class Card {
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Card_h */
|
||||
@@ -8,15 +8,25 @@
|
||||
|
||||
#include "DiskIICard.hpp"
|
||||
|
||||
using namespace AppleII;
|
||||
using namespace Apple::II;
|
||||
|
||||
DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector) : diskii_(2045454) {
|
||||
const auto roms = rom_fetcher(
|
||||
"DiskII",
|
||||
{
|
||||
is_16_sector ? "boot-16.rom" : "boot-13.rom",
|
||||
is_16_sector ? "state-machine-16.rom" : "state-machine-13.rom"
|
||||
std::vector<std::unique_ptr<std::vector<uint8_t>>> roms;
|
||||
if(is_16_sector) {
|
||||
roms = rom_fetcher({
|
||||
{"DiskII", "the Disk II 16-sector boot ROM", "boot-16.rom", 256, 0xce7144f6},
|
||||
{"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 } }
|
||||
});
|
||||
} else {
|
||||
roms = rom_fetcher({
|
||||
{"DiskII", "the Disk II 13-sector boot ROM", "boot-13.rom", 256, 0xd34eb2ff},
|
||||
{"DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620 }
|
||||
});
|
||||
}
|
||||
if(!roms[0] || !roms[1]) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
boot_ = std::move(*roms[0]);
|
||||
diskii_.set_state_machine(*roms[1]);
|
||||
set_select_constraints(None);
|
||||
@@ -10,17 +10,18 @@
|
||||
#define DiskIICard_hpp
|
||||
|
||||
#include "Card.hpp"
|
||||
#include "../ROMMachine.hpp"
|
||||
#include "../../ROMMachine.hpp"
|
||||
|
||||
#include "../../Components/DiskII/DiskII.hpp"
|
||||
#include "../../Storage/Disk/Disk.hpp"
|
||||
#include "../../ClockReceiver/ClockingHintSource.hpp"
|
||||
#include "../../../Components/DiskII/DiskII.hpp"
|
||||
#include "../../../Storage/Disk/Disk.hpp"
|
||||
#include "../../../ClockReceiver/ClockingHintSource.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace AppleII {
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
class DiskIICard: public Card, public ClockingHint::Observer {
|
||||
public:
|
||||
@@ -41,6 +42,7 @@ class DiskIICard: public Card, public ClockingHint::Observer {
|
||||
ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* DiskIICard_hpp */
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#include "Video.hpp"
|
||||
|
||||
using namespace AppleII::Video;
|
||||
using namespace Apple::II::Video;
|
||||
|
||||
VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
|
||||
crt_(910, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance1),
|
||||
@@ -9,14 +9,15 @@
|
||||
#ifndef Video_hpp
|
||||
#define Video_hpp
|
||||
|
||||
#include "../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../ClockReceiver/ClockDeferrer.hpp"
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../ClockReceiver/DeferredQueue.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace AppleII {
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
namespace Video {
|
||||
|
||||
class BusHandler {
|
||||
@@ -250,8 +251,8 @@ class VideoBase {
|
||||
*/
|
||||
void output_fat_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const;
|
||||
|
||||
// Maintain a ClockDeferrer for delayed mode switches.
|
||||
ClockDeferrer<Cycles> deferrer_;
|
||||
// Maintain a DeferredQueue for delayed mode switches.
|
||||
DeferredQueue<Cycles> deferrer_;
|
||||
};
|
||||
|
||||
template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
@@ -600,6 +601,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
BusHandler &bus_handler_;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
97
Machines/Apple/Macintosh/Audio.cpp
Normal file
97
Machines/Apple/Macintosh/Audio.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// Audio.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 31/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Audio.hpp"
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
namespace {
|
||||
|
||||
// The sample_length is coupled with the clock rate selected within the Macintosh proper;
|
||||
// as per the header-declaration a divide-by-two clock is expected to arrive here.
|
||||
const std::size_t sample_length = 352 / 2;
|
||||
|
||||
}
|
||||
|
||||
Audio::Audio(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {}
|
||||
|
||||
// MARK: - Inputs
|
||||
|
||||
void Audio::post_sample(uint8_t sample) {
|
||||
// Store sample directly indexed by current write pointer; this ensures that collected samples
|
||||
// directly map to volume and enabled/disabled states.
|
||||
sample_queue_.buffer[sample_queue_.write_pointer] = sample;
|
||||
sample_queue_.write_pointer = (sample_queue_.write_pointer + 1) % sample_queue_.buffer.size();
|
||||
}
|
||||
|
||||
void Audio::set_volume(int volume) {
|
||||
// Do nothing if the volume hasn't changed.
|
||||
if(posted_volume_ == volume) return;
|
||||
posted_volume_ = volume;
|
||||
|
||||
// Post the volume change as a deferred event.
|
||||
task_queue_.defer([=] () {
|
||||
volume_ = volume;
|
||||
set_volume_multiplier();
|
||||
});
|
||||
}
|
||||
|
||||
void Audio::set_enabled(bool on) {
|
||||
// Do nothing if the mask hasn't changed.
|
||||
if(posted_enable_mask_ == int(on)) return;
|
||||
posted_enable_mask_ = int(on);
|
||||
|
||||
// Post the enabled mask change as a deferred event.
|
||||
task_queue_.defer([=] () {
|
||||
enabled_mask_ = int(on);
|
||||
set_volume_multiplier();
|
||||
});
|
||||
}
|
||||
|
||||
// MARK: - Output generation
|
||||
|
||||
bool Audio::is_zero_level() {
|
||||
return !volume_ || !enabled_mask_;
|
||||
}
|
||||
|
||||
void Audio::set_sample_volume_range(std::int16_t range) {
|
||||
// Some underflow here doesn't really matter.
|
||||
output_volume_ = range / (7 * 255);
|
||||
set_volume_multiplier();
|
||||
}
|
||||
|
||||
void Audio::set_volume_multiplier() {
|
||||
volume_multiplier_ = int16_t(output_volume_ * volume_ * enabled_mask_);
|
||||
}
|
||||
|
||||
void Audio::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
// TODO: the implementation below acts as if the hardware uses pulse-amplitude modulation;
|
||||
// in fact it uses pulse-width modulation. But the scale for pulses isn't specified, so
|
||||
// that's something to return to.
|
||||
|
||||
while(number_of_samples) {
|
||||
// Determine how many output samples will be at the same level.
|
||||
const auto cycles_left_in_sample = std::min(number_of_samples, sample_length - subcycle_offset_);
|
||||
|
||||
// Determine the output level, and output that many samples.
|
||||
// (Hoping that the copiler substitutes an effective memset16-type operation here).
|
||||
const int16_t output_level = volume_multiplier_ * (int16_t(sample_queue_.buffer[sample_queue_.read_pointer]) - 128);
|
||||
for(size_t c = 0; c < cycles_left_in_sample; ++c) {
|
||||
target[c] = output_level;
|
||||
}
|
||||
target += cycles_left_in_sample;
|
||||
|
||||
// Advance the sample pointer.
|
||||
subcycle_offset_ += cycles_left_in_sample;
|
||||
sample_queue_.read_pointer = (sample_queue_.read_pointer + (subcycle_offset_ / sample_length)) % sample_queue_.buffer.size();
|
||||
subcycle_offset_ %= sample_length;
|
||||
|
||||
// Decreate the number of samples left to write.
|
||||
number_of_samples -= cycles_left_in_sample;
|
||||
}
|
||||
}
|
||||
88
Machines/Apple/Macintosh/Audio.hpp
Normal file
88
Machines/Apple/Macintosh/Audio.hpp
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// Audio.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 31/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Audio_hpp
|
||||
#define Audio_hpp
|
||||
|
||||
#include "../../../Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
/*!
|
||||
Implements the Macintosh's audio output hardware.
|
||||
|
||||
Designed to be clocked at half the rate of the real hardware — i.e.
|
||||
a shade less than 4Mhz.
|
||||
*/
|
||||
class Audio: public ::Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
Audio(Concurrency::DeferringAsyncTaskQueue &task_queue);
|
||||
|
||||
/*!
|
||||
Macintosh audio is (partly) sourced by the same scanning
|
||||
hardware as the video; each line it collects an additional
|
||||
word of memory, half of which is used for audio output.
|
||||
|
||||
Use this method to add a newly-collected sample to the queue.
|
||||
*/
|
||||
void post_sample(uint8_t sample);
|
||||
|
||||
/*!
|
||||
Macintosh audio also separately receives an output volume
|
||||
level, in the range 0 to 7.
|
||||
|
||||
Use this method to set the current output volume.
|
||||
*/
|
||||
void set_volume(int volume);
|
||||
|
||||
/*!
|
||||
A further factor in audio output is the on-off toggle.
|
||||
*/
|
||||
void set_enabled(bool on);
|
||||
|
||||
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
bool is_zero_level();
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||
|
||||
// A queue of fetched samples; read from by one thread,
|
||||
// written to by another.
|
||||
struct {
|
||||
std::array<uint8_t, 740> buffer;
|
||||
size_t read_pointer = 0, write_pointer = 0;
|
||||
} sample_queue_;
|
||||
|
||||
// Emulator-thread stateful variables, to avoid work posting
|
||||
// deferral updates if possible.
|
||||
int posted_volume_ = 0;
|
||||
int posted_enable_mask_ = 0;
|
||||
|
||||
// Stateful variables, modified from the audio generation
|
||||
// thread only.
|
||||
int volume_ = 0;
|
||||
int enabled_mask_ = 0;
|
||||
std::int16_t output_volume_ = 0;
|
||||
|
||||
std::int16_t volume_multiplier_ = 0;
|
||||
std::size_t subcycle_offset_ = 0;
|
||||
void set_volume_multiplier();
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Audio_hpp */
|
||||
34
Machines/Apple/Macintosh/DeferredAudio.hpp
Normal file
34
Machines/Apple/Macintosh/DeferredAudio.hpp
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// DeferredAudio.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 01/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef DeferredAudio_h
|
||||
#define DeferredAudio_h
|
||||
|
||||
#include "Audio.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
struct DeferredAudio {
|
||||
Concurrency::DeferringAsyncTaskQueue queue;
|
||||
Audio audio;
|
||||
Outputs::Speaker::LowpassSpeaker<Audio> speaker;
|
||||
HalfCycles time_since_update;
|
||||
|
||||
DeferredAudio() : audio(queue), speaker(audio) {}
|
||||
|
||||
void flush() {
|
||||
speaker.run_for(queue, time_since_update.flush<Cycles>());
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* DeferredAudio_h */
|
||||
63
Machines/Apple/Macintosh/DriveSpeedAccumulator.cpp
Normal file
63
Machines/Apple/Macintosh/DriveSpeedAccumulator.cpp
Normal file
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// DriveSpeedAccumulator.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 01/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "DriveSpeedAccumulator.hpp"
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
void DriveSpeedAccumulator::post_sample(uint8_t sample) {
|
||||
if(!number_of_drives_) return;
|
||||
|
||||
// An Euler-esque approximation is used here: just collect all
|
||||
// the samples until there is a certain small quantity of them,
|
||||
// then produce a new estimate of rotation speed and start the
|
||||
// buffer afresh.
|
||||
samples_[sample_pointer_] = sample;
|
||||
++sample_pointer_;
|
||||
|
||||
if(sample_pointer_ == samples_.size()) {
|
||||
sample_pointer_ = 0;
|
||||
|
||||
// The below fits for a function like `a + bc`; it encapsultes the following
|
||||
// beliefs:
|
||||
//
|
||||
// (i) motor speed is proportional to voltage supplied;
|
||||
// (ii) with pulse-width modulation it's therefore proportional to the duty cycle;
|
||||
// (iii) the Mac pulse-width modulates whatever it reads from the disk speed buffer;
|
||||
// (iv) ... subject to software pulse-width modulation of that pulse-width modulation.
|
||||
//
|
||||
// So, I believe current motor speed is proportional to a low-pass filtering of
|
||||
// the speed buffer. Which I've implemented very coarsely via 'large' bucketed-averages,
|
||||
// noting also that exact disk motor speed is always a little approximate.
|
||||
|
||||
// Sum all samples.
|
||||
// TODO: if the above is the correct test, do it on sample receipt rather than
|
||||
// bothering with an intermediate buffer.
|
||||
int sum = 0;
|
||||
for(auto s: samples_) {
|
||||
sum += s;
|
||||
}
|
||||
|
||||
// The formula below was derived from observing values the Mac wrote into its
|
||||
// disk-speed buffer. Given that it runs a calibration loop before doing so,
|
||||
// I cannot guarantee the accuracy of these numbers beyond being within the
|
||||
// range that the computer would accept.
|
||||
const float normalised_sum = float(sum) / float(samples_.size());
|
||||
const float rotation_speed = (normalised_sum * 27.08f) - 259.0f;
|
||||
|
||||
for(int c = 0; c < number_of_drives_; ++c) {
|
||||
drives_[c]->set_rotation_speed(rotation_speed);
|
||||
}
|
||||
// printf("RPM: %0.2f (%d sum)\n", rotation_speed, sum);
|
||||
}
|
||||
}
|
||||
|
||||
void DriveSpeedAccumulator::add_drive(Apple::Macintosh::DoubleDensityDrive *drive) {
|
||||
drives_[number_of_drives_] = drive;
|
||||
++number_of_drives_;
|
||||
}
|
||||
45
Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp
Normal file
45
Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// DriveSpeedAccumulator.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 01/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef DriveSpeedAccumulator_hpp
|
||||
#define DriveSpeedAccumulator_hpp
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
class DriveSpeedAccumulator {
|
||||
public:
|
||||
/*!
|
||||
Accepts fetched motor control values.
|
||||
*/
|
||||
void post_sample(uint8_t sample);
|
||||
|
||||
/*!
|
||||
Adds a connected drive. Up to two of these
|
||||
can be supplied. Only Macintosh DoubleDensityDrives
|
||||
are supported.
|
||||
*/
|
||||
void add_drive(Apple::Macintosh::DoubleDensityDrive *drive);
|
||||
|
||||
private:
|
||||
std::array<uint8_t, 20> samples_;
|
||||
std::size_t sample_pointer_ = 0;
|
||||
Apple::Macintosh::DoubleDensityDrive *drives_[2] = {nullptr, nullptr};
|
||||
int number_of_drives_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* DriveSpeedAccumulator_hpp */
|
||||
294
Machines/Apple/Macintosh/Keyboard.hpp
Normal file
294
Machines/Apple/Macintosh/Keyboard.hpp
Normal file
@@ -0,0 +1,294 @@
|
||||
//
|
||||
// Keyboard.h
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 08/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Apple_Macintosh_Keyboard_hpp
|
||||
#define Apple_Macintosh_Keyboard_hpp
|
||||
|
||||
#include "../../KeyboardMachine.hpp"
|
||||
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
class Keyboard {
|
||||
public:
|
||||
void set_input(bool data) {
|
||||
switch(mode_) {
|
||||
case Mode::Waiting:
|
||||
/*
|
||||
"Only the computer can initiate communication over the keyboard lines. When the computer and keyboard
|
||||
are turned on, the computer is in charge of the keyboard interface and the keyboard is passive. The
|
||||
computer signals that it is ready to begin communication by pulling the Keyboard Data line low."
|
||||
*/
|
||||
if(!data) {
|
||||
mode_ = Mode::AcceptingCommand;
|
||||
phase_ = 0;
|
||||
command_ = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case Mode::AcceptingCommand:
|
||||
/* Note value, so that it can be latched upon a clock transition. */
|
||||
data_input_ = data;
|
||||
break;
|
||||
|
||||
case Mode::AwaitingEndOfCommand:
|
||||
/*
|
||||
The last bit of the command leaves the Keyboard Data line low; the computer then indicates that it is ready
|
||||
to receive the keyboard's response by setting the Keyboard Data line high.
|
||||
*/
|
||||
if(data) {
|
||||
mode_ = Mode::PerformingCommand;
|
||||
phase_ = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
case Mode::SendingResponse:
|
||||
/* This line isn't currently an input; do nothing. */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool get_clock() {
|
||||
return clock_output_;
|
||||
}
|
||||
|
||||
bool get_data() {
|
||||
return !!(response_ & 0x80);
|
||||
}
|
||||
|
||||
/*!
|
||||
The keyboard expects ~10 µs-frequency ticks, i.e. a clock rate of just around 100 kHz.
|
||||
*/
|
||||
void run_for(HalfCycles cycle) {
|
||||
switch(mode_) {
|
||||
default:
|
||||
case Mode::Waiting: return;
|
||||
|
||||
case Mode::AcceptingCommand: {
|
||||
/*
|
||||
"When the computer is sending data to the keyboard, the keyboard transmits eight cycles of 400 µS each (180 µS low,
|
||||
220 µS high) on the Keyboard Clock line. On the falling edge of each keyboard clock cycle, the Macintosh Plus places
|
||||
a data bit on the data line and holds it there for 400 µS. The keyboard reads the data bit 80 µS after the rising edge
|
||||
of the Keyboard Clock signal."
|
||||
*/
|
||||
const auto offset = phase_ % 40;
|
||||
clock_output_ = offset >= 18;
|
||||
|
||||
if(offset == 26) {
|
||||
command_ = (command_ << 1) | (data_input_ ? 1 : 0);
|
||||
}
|
||||
|
||||
++phase_;
|
||||
if(phase_ == 8*40) {
|
||||
mode_ = Mode::AwaitingEndOfCommand;
|
||||
phase_ = 0;
|
||||
clock_output_ = false;
|
||||
}
|
||||
} break;
|
||||
|
||||
case Mode::AwaitingEndOfCommand:
|
||||
// Time out if the end-of-command seems not to be forthcoming.
|
||||
// This is an elaboration on my part; a guess.
|
||||
++phase_;
|
||||
if(phase_ == 1000) {
|
||||
clock_output_ = false;
|
||||
mode_ = Mode::Waiting;
|
||||
phase_ = 0;
|
||||
}
|
||||
return;
|
||||
|
||||
case Mode::PerformingCommand: {
|
||||
response_ = perform_command(command_);
|
||||
|
||||
// Inquiry has a 0.25-second timeout; everything else is instant.
|
||||
++phase_;
|
||||
if(phase_ == 25000 || command_ != 0x10 || response_ != 0x7b) {
|
||||
mode_ = Mode::SendingResponse;
|
||||
phase_ = 0;
|
||||
}
|
||||
} break;
|
||||
|
||||
case Mode::SendingResponse: {
|
||||
/*
|
||||
"When sending data to the computer, the keyboard transmits eight cycles of 330 µS each (160 µS low, 170 µS high)
|
||||
on the normally high Keyboard Clock line. It places a data bit on the data line 40 µS before the falling edge of each
|
||||
clock cycle and maintains it for 330 µS. The VIA in the computer latches the data bit into its shift register on the
|
||||
rising edge of the Keyboard Clock signal."
|
||||
*/
|
||||
const auto offset = phase_ % 33;
|
||||
clock_output_ = offset >= 16;
|
||||
|
||||
if(offset == 29) {
|
||||
response_ <<= 1;
|
||||
}
|
||||
|
||||
++phase_;
|
||||
if(phase_ == 8*33) {
|
||||
clock_output_ = false;
|
||||
mode_ = Mode::Waiting;
|
||||
phase_ = 0;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void enqueue_key_state(uint16_t key, bool is_pressed) {
|
||||
// Front insert; messages will be pop_back'd.
|
||||
std::lock_guard<decltype(key_queue_mutex_)> lock(key_queue_mutex_);
|
||||
|
||||
// Keys on the keypad are preceded by a $79 keycode; in the internal naming scheme
|
||||
// they are indicated by having bit 8 set. So add the $79 prefix if required.
|
||||
if(key & 0x100) {
|
||||
key_queue_.insert(key_queue_.begin(), 0x79);
|
||||
}
|
||||
key_queue_.insert(key_queue_.begin(), (is_pressed ? 0x00 : 0x80) | uint8_t(key));
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
int perform_command(int command) {
|
||||
switch(command) {
|
||||
case 0x10: // Inquiry.
|
||||
case 0x14: { // Instant.
|
||||
std::lock_guard<decltype(key_queue_mutex_)> lock(key_queue_mutex_);
|
||||
if(!key_queue_.empty()) {
|
||||
const auto new_message = key_queue_.back();
|
||||
key_queue_.pop_back();
|
||||
return new_message;
|
||||
}
|
||||
} break;
|
||||
|
||||
case 0x16: // Model number.
|
||||
return
|
||||
0x01 | // b0: always 1
|
||||
(1 << 1) | // keyboard model number
|
||||
(1 << 4); // next device number
|
||||
// (b7 not set => no next device)
|
||||
|
||||
case 0x36: // Test
|
||||
return 0x7d; // 0x7d = ACK, 0x77 = not ACK.
|
||||
}
|
||||
return 0x7b; // No key transition.
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
Waiting,
|
||||
AcceptingCommand,
|
||||
AwaitingEndOfCommand,
|
||||
SendingResponse,
|
||||
PerformingCommand
|
||||
} mode_ = Mode::Waiting;
|
||||
int phase_ = 0;
|
||||
int command_ = 0;
|
||||
int response_ = 0;
|
||||
|
||||
bool data_input_ = false;
|
||||
bool clock_output_ = false;
|
||||
|
||||
// TODO: improve this very, very simple implementation.
|
||||
std::mutex key_queue_mutex_;
|
||||
std::vector<uint8_t> key_queue_;
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides a mapping from idiomatic PC keys to Macintosh keys.
|
||||
*/
|
||||
class KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) override {
|
||||
using Key = Inputs::Keyboard::Key;
|
||||
switch(key) {
|
||||
default: return KeyboardMachine::MappedMachine::KeyNotMapped;
|
||||
|
||||
/*
|
||||
See p284 of the Apple Guide to the Macintosh Family Hardware
|
||||
for documentation of the mapping below.
|
||||
*/
|
||||
|
||||
case Key::BackTick: return 0x65;
|
||||
case Key::k1: return 0x25;
|
||||
case Key::k2: return 0x27;
|
||||
case Key::k3: return 0x29;
|
||||
case Key::k4: return 0x2b;
|
||||
case Key::k5: return 0x2f;
|
||||
case Key::k6: return 0x2d;
|
||||
case Key::k7: return 0x35;
|
||||
case Key::k8: return 0x39;
|
||||
case Key::k9: return 0x33;
|
||||
case Key::k0: return 0x3b;
|
||||
case Key::Hyphen: return 0x37;
|
||||
case Key::Equals: return 0x31;
|
||||
case Key::BackSpace: return 0x67;
|
||||
|
||||
case Key::Tab: return 0x61;
|
||||
case Key::Q: return 0x19;
|
||||
case Key::W: return 0x1b;
|
||||
case Key::E: return 0x1d;
|
||||
case Key::R: return 0x1f;
|
||||
case Key::T: return 0x23;
|
||||
case Key::Y: return 0x21;
|
||||
case Key::U: return 0x41;
|
||||
case Key::I: return 0x45;
|
||||
case Key::O: return 0x3f;
|
||||
case Key::P: return 0x47;
|
||||
case Key::OpenSquareBracket: return 0x43;
|
||||
case Key::CloseSquareBracket: return 0x3d;
|
||||
|
||||
case Key::CapsLock: return 0x73;
|
||||
case Key::A: return 0x01;
|
||||
case Key::S: return 0x03;
|
||||
case Key::D: return 0x05;
|
||||
case Key::F: return 0x07;
|
||||
case Key::G: return 0x0b;
|
||||
case Key::H: return 0x09;
|
||||
case Key::J: return 0x4d;
|
||||
case Key::K: return 0x51;
|
||||
case Key::L: return 0x4b;
|
||||
case Key::Semicolon: return 0x53;
|
||||
case Key::Quote: return 0x4f;
|
||||
case Key::Enter: return 0x49;
|
||||
|
||||
case Key::LeftShift: return 0x71;
|
||||
case Key::Z: return 0x0d;
|
||||
case Key::X: return 0x0f;
|
||||
case Key::C: return 0x11;
|
||||
case Key::V: return 0x13;
|
||||
case Key::B: return 0x17;
|
||||
case Key::N: return 0x5b;
|
||||
case Key::M: return 0x5d;
|
||||
case Key::Comma: return 0x57;
|
||||
case Key::FullStop: return 0x5f;
|
||||
case Key::ForwardSlash: return 0x59;
|
||||
case Key::RightShift: return 0x71;
|
||||
|
||||
case Key::Left: return 0x100 | 0x0d;
|
||||
case Key::Right: return 0x100 | 0x05;
|
||||
case Key::Up: return 0x100 | 0x1b;
|
||||
case Key::Down: return 0x100 | 0x11;
|
||||
|
||||
case Key::LeftOption:
|
||||
case Key::RightOption: return 0x75;
|
||||
case Key::LeftMeta:
|
||||
case Key::RightMeta: return 0x6f;
|
||||
|
||||
case Key::Space: return 0x63;
|
||||
case Key::BackSlash: return 0x55;
|
||||
|
||||
/* TODO: the numeric keypad. */
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Apple_Macintosh_Keyboard_hpp */
|
||||
653
Machines/Apple/Macintosh/Macintosh.cpp
Normal file
653
Machines/Apple/Macintosh/Macintosh.cpp
Normal file
@@ -0,0 +1,653 @@
|
||||
//
|
||||
// Macintosh.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 03/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Macintosh.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "DeferredAudio.hpp"
|
||||
#include "DriveSpeedAccumulator.hpp"
|
||||
#include "Keyboard.hpp"
|
||||
#include "RealTimeClock.hpp"
|
||||
#include "Video.hpp"
|
||||
|
||||
#include "../../CRTMachine.hpp"
|
||||
#include "../../KeyboardMachine.hpp"
|
||||
#include "../../MediaTarget.hpp"
|
||||
#include "../../MouseMachine.hpp"
|
||||
|
||||
#include "../../../Inputs/QuadratureMouse/QuadratureMouse.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include "../../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
//#define LOG_TRACE
|
||||
|
||||
#include "../../../Components/6522/6522.hpp"
|
||||
#include "../../../Components/8530/z8530.hpp"
|
||||
#include "../../../Components/DiskII/IWM.hpp"
|
||||
#include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp"
|
||||
#include "../../../Processors/68000/68000.hpp"
|
||||
|
||||
#include "../../../Analyser/Static/Macintosh/Target.hpp"
|
||||
|
||||
#include "../../Utility/MemoryPacker.hpp"
|
||||
#include "../../Utility/MemoryFuzzer.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
const int CLOCK_RATE = 7833600;
|
||||
|
||||
}
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachine:
|
||||
public Machine,
|
||||
public CRTMachine::Machine,
|
||||
public MediaTarget::Machine,
|
||||
public MouseMachine::Machine,
|
||||
public CPU::MC68000::BusHandler,
|
||||
public KeyboardMachine::MappedMachine,
|
||||
public Zilog::SCC::z8530::Delegate {
|
||||
public:
|
||||
using Target = Analyser::Static::Macintosh::Target;
|
||||
|
||||
ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
mc68000_(*this),
|
||||
iwm_(CLOCK_RATE),
|
||||
video_(ram_, audio_, drive_speed_accumulator_),
|
||||
via_(via_port_handler_),
|
||||
via_port_handler_(*this, clock_, keyboard_, video_, audio_, iwm_, mouse_),
|
||||
drives_{
|
||||
{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke},
|
||||
{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke}
|
||||
},
|
||||
mouse_(1) {
|
||||
|
||||
// Select a ROM name and determine the proper ROM and RAM sizes
|
||||
// based on the machine model.
|
||||
using Model = Analyser::Static::Macintosh::Target::Model;
|
||||
const std::string machine_name = "Macintosh";
|
||||
uint32_t ram_size, rom_size;
|
||||
std::vector<ROMMachine::ROM> rom_descriptions;
|
||||
switch(model) {
|
||||
default:
|
||||
case Model::Mac128k:
|
||||
ram_size = 128*1024;
|
||||
rom_size = 64*1024;
|
||||
rom_descriptions.emplace_back(machine_name, "the Macintosh 128k ROM", "mac128k.rom", 64*1024, 0x6d0c8a28);
|
||||
break;
|
||||
case Model::Mac512k:
|
||||
ram_size = 512*1024;
|
||||
rom_size = 64*1024;
|
||||
rom_descriptions.emplace_back(machine_name, "the Macintosh 512k ROM", "mac512k.rom", 64*1024, 0xcf759e0d);
|
||||
break;
|
||||
case Model::Mac512ke:
|
||||
case Model::MacPlus: {
|
||||
ram_size = 512*1024;
|
||||
rom_size = 128*1024;
|
||||
const std::initializer_list<uint32_t> crc32s = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e };
|
||||
rom_descriptions.emplace_back(machine_name, "the Macintosh Plus ROM", "macplus.rom", 128*1024, crc32s);
|
||||
} break;
|
||||
}
|
||||
ram_mask_ = (ram_size >> 1) - 1;
|
||||
rom_mask_ = (rom_size >> 1) - 1;
|
||||
video_.set_ram_mask(ram_mask_);
|
||||
|
||||
// Grab a copy of the ROM and convert it into big-endian data.
|
||||
const auto roms = rom_fetcher(rom_descriptions);
|
||||
if(!roms[0]) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
roms[0]->resize(rom_size);
|
||||
Memory::PackBigEndian16(*roms[0], rom_);
|
||||
|
||||
// Randomise memory contents.
|
||||
Memory::Fuzz(ram_, sizeof(ram_) / sizeof(*ram_));
|
||||
|
||||
// Attach the drives to the IWM.
|
||||
iwm_.iwm.set_drive(0, &drives_[0]);
|
||||
iwm_.iwm.set_drive(1, &drives_[1]);
|
||||
|
||||
// If they are 400kb drives, also attach them to the drive-speed accumulator.
|
||||
if(!drives_[0].is_800k()) drive_speed_accumulator_.add_drive(&drives_[0]);
|
||||
if(!drives_[1].is_800k()) drive_speed_accumulator_.add_drive(&drives_[1]);
|
||||
|
||||
// Make sure interrupt changes from the SCC are observed.
|
||||
scc_.set_delegate(this);
|
||||
|
||||
// The Mac runs at 7.8336mHz.
|
||||
set_clock_rate(double(CLOCK_RATE));
|
||||
audio_.speaker.set_input_rate(float(CLOCK_RATE) / 2.0f);
|
||||
|
||||
// Insert any supplied media.
|
||||
insert_media(target.media);
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_.queue.flush();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
video_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
return &audio_.speaker;
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
mc68000_.run_for(cycles);
|
||||
}
|
||||
|
||||
using Microcycle = CPU::MC68000::Microcycle;
|
||||
|
||||
HalfCycles perform_bus_operation(const Microcycle &cycle, int is_supervisor) {
|
||||
// TODO: pick a delay if this is a video-clashing memory fetch.
|
||||
HalfCycles delay(0);
|
||||
|
||||
time_since_video_update_ += cycle.length;
|
||||
iwm_.time_since_update += cycle.length;
|
||||
|
||||
// The VIA runs at one-tenth of the 68000's clock speed, in sync with the E clock.
|
||||
// See: Guide to the Macintosh Hardware Family p149 (PDF p188). Some extra division
|
||||
// may occur here in order to provide VSYNC at a proper moment.
|
||||
// Possibly route vsync.
|
||||
if(time_since_video_update_ < time_until_video_event_) {
|
||||
via_clock_ += cycle.length;
|
||||
via_.run_for(via_clock_.divide(HalfCycles(10)));
|
||||
} else {
|
||||
auto via_time_base = time_since_video_update_ - cycle.length;
|
||||
auto via_cycles_outstanding = cycle.length;
|
||||
while(time_until_video_event_ < time_since_video_update_) {
|
||||
const auto via_cycles = time_until_video_event_ - via_time_base;
|
||||
via_time_base = HalfCycles(0);
|
||||
via_cycles_outstanding -= via_cycles;
|
||||
|
||||
via_clock_ += via_cycles;
|
||||
via_.run_for(via_clock_.divide(HalfCycles(10)));
|
||||
|
||||
video_.run_for(time_until_video_event_);
|
||||
time_since_video_update_ -= time_until_video_event_;
|
||||
time_until_video_event_ = video_.get_next_sequence_point();
|
||||
|
||||
via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::One, !video_.vsync());
|
||||
}
|
||||
|
||||
via_clock_ += via_cycles_outstanding;
|
||||
via_.run_for(via_clock_.divide(HalfCycles(10)));
|
||||
}
|
||||
|
||||
// The keyboard also has a clock, albeit a very slow one — 100,000 cycles/second.
|
||||
// Its clock and data lines are connected to the VIA.
|
||||
keyboard_clock_ += cycle.length;
|
||||
const auto keyboard_ticks = keyboard_clock_.divide(HalfCycles(CLOCK_RATE / 100000));
|
||||
if(keyboard_ticks > HalfCycles(0)) {
|
||||
keyboard_.run_for(keyboard_ticks);
|
||||
via_.set_control_line_input(MOS::MOS6522::Port::B, MOS::MOS6522::Line::Two, keyboard_.get_data());
|
||||
via_.set_control_line_input(MOS::MOS6522::Port::B, MOS::MOS6522::Line::One, keyboard_.get_clock());
|
||||
}
|
||||
|
||||
// Feed mouse inputs within at most 1250 cycles of each other.
|
||||
if(mouse_.has_steps()) {
|
||||
time_since_mouse_update_ += cycle.length;
|
||||
const auto mouse_ticks = time_since_mouse_update_.divide(HalfCycles(2500));
|
||||
if(mouse_ticks > HalfCycles(0)) {
|
||||
mouse_.prepare_step();
|
||||
scc_.set_dcd(0, mouse_.get_channel(1) & 1);
|
||||
scc_.set_dcd(1, mouse_.get_channel(0) & 1);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: SCC should be clocked at a divide-by-two, if and when it actually has
|
||||
// anything connected.
|
||||
|
||||
// Consider updating the real-time clock.
|
||||
real_time_clock_ += cycle.length;
|
||||
auto ticks = real_time_clock_.divide_cycles(Cycles(CLOCK_RATE)).as_int();
|
||||
while(ticks--) {
|
||||
clock_.update();
|
||||
// TODO: leave a delay between toggling the input rather than using this coupled hack.
|
||||
via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two, true);
|
||||
via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two, false);
|
||||
}
|
||||
|
||||
// A null cycle leaves nothing else to do.
|
||||
if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return delay;
|
||||
|
||||
auto word_address = cycle.active_operation_word_address();
|
||||
|
||||
// Everything above E0 0000 is signalled as being on the peripheral bus.
|
||||
mc68000_.set_is_peripheral_address(word_address >= 0x700000);
|
||||
|
||||
// All code below deals only with reads and writes — cycles in which a
|
||||
// data select is active. So quit now if this is not the active part of
|
||||
// a read or write.
|
||||
if(!cycle.data_select_active()) return delay;
|
||||
|
||||
// Check whether this access maps into the IO area; if so then
|
||||
// apply more complicated decoding logic.
|
||||
if(word_address >= 0x400000) {
|
||||
const int register_address = word_address >> 8;
|
||||
|
||||
switch(word_address & 0x78f000) {
|
||||
case 0x70f000:
|
||||
// VIA accesses are via address 0xefe1fe + register*512,
|
||||
// which at word precision is 0x77f0ff + register*256.
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = via_.get_register(register_address);
|
||||
} else {
|
||||
via_.set_register(register_address, cycle.value->halves.low);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x68f000:
|
||||
// The IWM; this is a purely polled device, so can be run on demand.
|
||||
iwm_.flush();
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = iwm_.iwm.read(register_address);
|
||||
} else {
|
||||
iwm_.iwm.write(register_address, cycle.value->halves.low);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x780000:
|
||||
// Phase read.
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = phase_ & 7;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x480000: case 0x48f000:
|
||||
case 0x580000: case 0x58f000:
|
||||
// Any word access here adjusts phase.
|
||||
if(cycle.operation & Microcycle::SelectWord) {
|
||||
++phase_;
|
||||
} else {
|
||||
if(word_address < 0x500000) {
|
||||
// A0 = 1 => reset; A0 = 0 => read.
|
||||
if(*cycle.address & 1) {
|
||||
scc_.reset();
|
||||
} else {
|
||||
const auto read = scc_.read(int(word_address));
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
cycle.value->halves.low = read;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(*cycle.address & 1) {
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
scc_.write(int(word_address), 0xff);
|
||||
} else {
|
||||
scc_.write(int(word_address), cycle.value->halves.low);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
LOG("Unrecognised read " << PADHEX(6) << (*cycle.address & 0xffffff));
|
||||
cycle.value->halves.low = 0x00;
|
||||
} else {
|
||||
LOG("Unrecognised write %06x" << PADHEX(6) << (*cycle.address & 0xffffff));
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff;
|
||||
|
||||
return delay;
|
||||
}
|
||||
|
||||
// Having reached here, this is a RAM or ROM access.
|
||||
|
||||
// When ROM overlay is enabled, the ROM begins at both $000000 and $400000,
|
||||
// and RAM is available at $600000.
|
||||
//
|
||||
// Otherwise RAM is mapped at $000000 and ROM from $400000.
|
||||
uint16_t *memory_base;
|
||||
if(
|
||||
(!ROM_is_overlay_ && word_address < 0x200000) ||
|
||||
(ROM_is_overlay_ && word_address >= 0x300000)
|
||||
) {
|
||||
memory_base = ram_;
|
||||
word_address &= ram_mask_;
|
||||
|
||||
// This is coupled with the Macintosh implementation of video; the magic
|
||||
// constant should probably be factored into the Video class.
|
||||
// It embodies knowledge of the fact that video (and audio) will always
|
||||
// be fetched from the final $d900 bytes (i.e. $6c80 words) of memory.
|
||||
// (And that ram_mask_ = ram size - 1).
|
||||
// if(word_address > ram_mask_ - 0x6c80)
|
||||
update_video();
|
||||
} else {
|
||||
memory_base = rom_;
|
||||
word_address &= rom_mask_;
|
||||
|
||||
// Writes to ROM have no effect, and it doesn't mirror above 0x60000.
|
||||
if(!(cycle.operation & Microcycle::Read)) return delay;
|
||||
if(word_address >= 0x300000) {
|
||||
if(cycle.operation & Microcycle::SelectWord) {
|
||||
cycle.value->full = 0xffff;
|
||||
} else {
|
||||
cycle.value->halves.low = 0xff;
|
||||
}
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
|
||||
switch(cycle.operation & (Microcycle::SelectWord | Microcycle::SelectByte | Microcycle::Read | Microcycle::InterruptAcknowledge)) {
|
||||
default:
|
||||
break;
|
||||
|
||||
// Catches the deliberation set of operation to 0 above.
|
||||
case 0: break;
|
||||
|
||||
case Microcycle::InterruptAcknowledge | Microcycle::SelectByte:
|
||||
// The Macintosh uses autovectored interrupts.
|
||||
mc68000_.set_is_peripheral_address(true);
|
||||
break;
|
||||
|
||||
case Microcycle::SelectWord | Microcycle::Read:
|
||||
cycle.value->full = memory_base[word_address];
|
||||
break;
|
||||
case Microcycle::SelectByte | Microcycle::Read:
|
||||
cycle.value->halves.low = uint8_t(memory_base[word_address] >> cycle.byte_shift());
|
||||
break;
|
||||
case Microcycle::SelectWord:
|
||||
memory_base[word_address] = cycle.value->full;
|
||||
break;
|
||||
case Microcycle::SelectByte:
|
||||
memory_base[word_address] = uint16_t(
|
||||
(cycle.value->halves.low << cycle.byte_shift()) |
|
||||
(memory_base[word_address] & cycle.untouched_byte_mask())
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
Normal memory map:
|
||||
|
||||
000000: RAM
|
||||
400000: ROM
|
||||
9FFFF8+: SCC read operations
|
||||
BFFFF8+: SCC write operations
|
||||
DFE1FF+: IWM
|
||||
EFE1FE+: VIA
|
||||
*/
|
||||
|
||||
return delay;
|
||||
}
|
||||
|
||||
void flush() {
|
||||
// Flush the video before the audio queue; in a Mac the
|
||||
// video is responsible for providing part of the
|
||||
// audio signal, so the two aren't as distinct as in
|
||||
// most machines.
|
||||
update_video();
|
||||
|
||||
// As above: flush audio after video.
|
||||
via_.flush();
|
||||
audio_.queue.perform();
|
||||
|
||||
// Experimental?
|
||||
iwm_.flush();
|
||||
}
|
||||
|
||||
void set_rom_is_overlay(bool rom_is_overlay) {
|
||||
ROM_is_overlay_ = rom_is_overlay;
|
||||
}
|
||||
|
||||
bool video_is_outputting() {
|
||||
return video_.is_outputting(time_since_video_update_);
|
||||
}
|
||||
|
||||
void set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) {
|
||||
update_video();
|
||||
video_.set_use_alternate_buffers(use_alternate_screen_buffer, use_alternate_audio_buffer);
|
||||
}
|
||||
|
||||
bool insert_media(const Analyser::Static::Media &media) override {
|
||||
if(media.disks.empty())
|
||||
return false;
|
||||
|
||||
// TODO: shouldn't allow disks to be replaced like this, as the Mac
|
||||
// uses software eject. Will need to expand messaging ability of
|
||||
// insert_media.
|
||||
if(drives_[0].has_disk())
|
||||
drives_[1].set_disk(media.disks[0]);
|
||||
else
|
||||
drives_[0].set_disk(media.disks[0]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: Keyboard input.
|
||||
|
||||
KeyboardMapper *get_keyboard_mapper() override {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
|
||||
void set_key_state(uint16_t key, bool is_pressed) override {
|
||||
keyboard_.enqueue_key_state(key, is_pressed);
|
||||
}
|
||||
|
||||
// TODO: clear all keys.
|
||||
|
||||
// MARK: Interrupt updates.
|
||||
|
||||
void did_change_interrupt_status(Zilog::SCC::z8530 *sender, bool new_status) override {
|
||||
update_interrupt_input();
|
||||
}
|
||||
|
||||
void update_interrupt_input() {
|
||||
// Update interrupt input.
|
||||
// TODO: does this really cascade like this?
|
||||
if(scc_.get_interrupt_line()) {
|
||||
mc68000_.set_interrupt_level(2);
|
||||
} else if(via_.get_interrupt_line()) {
|
||||
mc68000_.set_interrupt_level(1);
|
||||
} else {
|
||||
mc68000_.set_interrupt_level(0);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void update_video() {
|
||||
video_.run_for(time_since_video_update_.flush<HalfCycles>());
|
||||
time_until_video_event_ = video_.get_next_sequence_point();
|
||||
}
|
||||
|
||||
Inputs::Mouse &get_mouse() override {
|
||||
return mouse_;
|
||||
}
|
||||
|
||||
struct IWM {
|
||||
IWM(int clock_rate) : iwm(clock_rate) {}
|
||||
|
||||
HalfCycles time_since_update;
|
||||
Apple::IWM iwm;
|
||||
|
||||
void flush() {
|
||||
iwm.run_for(time_since_update.flush<Cycles>());
|
||||
}
|
||||
};
|
||||
|
||||
class VIAPortHandler: public MOS::MOS6522::PortHandler {
|
||||
public:
|
||||
VIAPortHandler(ConcreteMachine &machine, RealTimeClock &clock, Keyboard &keyboard, Video &video, DeferredAudio &audio, IWM &iwm, Inputs::QuadratureMouse &mouse) :
|
||||
machine_(machine), clock_(clock), keyboard_(keyboard), video_(video), audio_(audio), iwm_(iwm), mouse_(mouse) {}
|
||||
|
||||
using Port = MOS::MOS6522::Port;
|
||||
using Line = MOS::MOS6522::Line;
|
||||
|
||||
void set_port_output(Port port, uint8_t value, uint8_t direction_mask) {
|
||||
/*
|
||||
Peripheral lines: keyboard data, interrupt configuration.
|
||||
(See p176 [/215])
|
||||
*/
|
||||
switch(port) {
|
||||
case Port::A:
|
||||
/*
|
||||
Port A:
|
||||
b7: [input] SCC wait/request (/W/REQA and /W/REQB wired together for a logical OR)
|
||||
b6: 0 = alternate screen buffer, 1 = main screen buffer
|
||||
b5: floppy disk SEL state control (upper/lower head "among other things")
|
||||
b4: 1 = use ROM overlay memory map, 0 = use ordinary memory map
|
||||
b3: 0 = use alternate sound buffer, 1 = use ordinary sound buffer
|
||||
b2–b0: audio output volume
|
||||
*/
|
||||
iwm_.flush();
|
||||
iwm_.iwm.set_select(!!(value & 0x20));
|
||||
|
||||
machine_.set_use_alternate_buffers(!(value & 0x40), !(value&0x08));
|
||||
machine_.set_rom_is_overlay(!!(value & 0x10));
|
||||
|
||||
audio_.flush();
|
||||
audio_.audio.set_volume(value & 7);
|
||||
break;
|
||||
|
||||
case Port::B:
|
||||
/*
|
||||
Port B:
|
||||
b7: 0 = sound enabled, 1 = sound disabled
|
||||
b6: [input] 0 = video beam in visible portion of line, 1 = outside
|
||||
b5: [input] mouse y2
|
||||
b4: [input] mouse x2
|
||||
b3: [input] 0 = mouse button down, 1 = up
|
||||
b2: 0 = real-time clock enabled, 1 = disabled
|
||||
b1: clock's data-clock line
|
||||
b0: clock's serial data line
|
||||
*/
|
||||
if(value & 0x4) clock_.abort();
|
||||
else clock_.set_input(!!(value & 0x2), !!(value & 0x1));
|
||||
|
||||
audio_.flush();
|
||||
audio_.audio.set_enabled(!(value & 0x80));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t get_port_input(Port port) {
|
||||
switch(port) {
|
||||
case Port::A:
|
||||
// printf("6522 r A\n");
|
||||
return 0x00; // TODO: b7 = SCC wait/request
|
||||
|
||||
case Port::B:
|
||||
return uint8_t(
|
||||
((mouse_.get_button_mask() & 1) ? 0x00 : 0x08) |
|
||||
((mouse_.get_channel(0) & 2) << 3) |
|
||||
((mouse_.get_channel(1) & 2) << 4) |
|
||||
(clock_.get_data() ? 0x02 : 0x00) |
|
||||
(machine_.video_is_outputting() ? 0x00 : 0x40)
|
||||
);
|
||||
}
|
||||
|
||||
// Should be unreachable.
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
void set_control_line_output(Port port, Line line, bool value) {
|
||||
/*
|
||||
Keyboard wiring (I believe):
|
||||
CB2 = data (input/output)
|
||||
CB1 = clock (input)
|
||||
|
||||
CA2 is used for receiving RTC interrupts.
|
||||
CA1 is used for receiving vsync.
|
||||
*/
|
||||
if(port == Port::B && line == Line::Two) {
|
||||
keyboard_.set_input(value);
|
||||
}
|
||||
else LOG("Unhandled control line output: " << (port ? 'B' : 'A') << int(line));
|
||||
}
|
||||
|
||||
void run_for(HalfCycles duration) {
|
||||
// The 6522 enjoys a divide-by-ten, so multiply back up here to make the
|
||||
// divided-by-two clock the audio works on.
|
||||
audio_.time_since_update += HalfCycles(duration.as_int() * 5);
|
||||
}
|
||||
|
||||
void flush() {
|
||||
audio_.flush();
|
||||
}
|
||||
|
||||
void set_interrupt_status(bool status) {
|
||||
machine_.update_interrupt_input();
|
||||
}
|
||||
|
||||
private:
|
||||
ConcreteMachine &machine_;
|
||||
RealTimeClock &clock_;
|
||||
Keyboard &keyboard_;
|
||||
Video &video_;
|
||||
DeferredAudio &audio_;
|
||||
IWM &iwm_;
|
||||
Inputs::QuadratureMouse &mouse_;
|
||||
};
|
||||
|
||||
CPU::MC68000::Processor<ConcreteMachine, true> mc68000_;
|
||||
|
||||
DriveSpeedAccumulator drive_speed_accumulator_;
|
||||
IWM iwm_;
|
||||
|
||||
DeferredAudio audio_;
|
||||
Video video_;
|
||||
|
||||
RealTimeClock clock_;
|
||||
Keyboard keyboard_;
|
||||
|
||||
MOS::MOS6522::MOS6522<VIAPortHandler> via_;
|
||||
VIAPortHandler via_port_handler_;
|
||||
|
||||
Zilog::SCC::z8530 scc_;
|
||||
|
||||
HalfCycles via_clock_;
|
||||
HalfCycles real_time_clock_;
|
||||
HalfCycles keyboard_clock_;
|
||||
HalfCycles time_since_video_update_;
|
||||
HalfCycles time_until_video_event_;
|
||||
HalfCycles time_since_mouse_update_;
|
||||
|
||||
bool ROM_is_overlay_ = true;
|
||||
int phase_ = 1;
|
||||
|
||||
DoubleDensityDrive drives_[2];
|
||||
Inputs::QuadratureMouse mouse_;
|
||||
|
||||
Apple::Macintosh::KeyboardMapper keyboard_mapper_;
|
||||
|
||||
uint32_t ram_mask_ = 0;
|
||||
uint32_t rom_mask_ = 0;
|
||||
uint16_t rom_[64*1024];
|
||||
uint16_t ram_[256*1024];
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
Machine *Machine::Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
auto *const mac_target = dynamic_cast<const Analyser::Static::Macintosh::Target *>(target);
|
||||
|
||||
using Model = Analyser::Static::Macintosh::Target::Model;
|
||||
switch(mac_target->model) {
|
||||
default:
|
||||
case Model::Mac128k: return new ConcreteMachine<Model::Mac128k>(*mac_target, rom_fetcher);
|
||||
case Model::Mac512k: return new ConcreteMachine<Model::Mac512k>(*mac_target, rom_fetcher);
|
||||
case Model::Mac512ke: return new ConcreteMachine<Model::Mac512ke>(*mac_target, rom_fetcher);
|
||||
case Model::MacPlus: return new ConcreteMachine<Model::MacPlus>(*mac_target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
30
Machines/Apple/Macintosh/Macintosh.hpp
Normal file
30
Machines/Apple/Macintosh/Macintosh.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Macintosh.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 03/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Macintosh_hpp
|
||||
#define Macintosh_hpp
|
||||
|
||||
#include "../../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../../ROMMachine.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
|
||||
/// Creates and returns a Macintosh.
|
||||
static Machine *Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Macintosh_hpp */
|
||||
166
Machines/Apple/Macintosh/RealTimeClock.hpp
Normal file
166
Machines/Apple/Macintosh/RealTimeClock.hpp
Normal file
@@ -0,0 +1,166 @@
|
||||
//
|
||||
// RealTimeClock.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef RealTimeClock_hpp
|
||||
#define RealTimeClock_hpp
|
||||
|
||||
#include "../../Utility/MemoryFuzzer.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
/*!
|
||||
Models the storage component of Apple's real-time clock.
|
||||
|
||||
Since tracking of time is pushed to this class, it is assumed
|
||||
that whomever is translating real time into emulated time
|
||||
will notify the VIA of a potential interrupt.
|
||||
*/
|
||||
class RealTimeClock {
|
||||
public:
|
||||
RealTimeClock() {
|
||||
// TODO: this should persist, if possible, rather than
|
||||
// being randomly initialised.
|
||||
Memory::Fuzz(data_, sizeof(data_));
|
||||
}
|
||||
|
||||
/*!
|
||||
Advances the clock by 1 second.
|
||||
|
||||
The caller should also notify the VIA.
|
||||
*/
|
||||
void update() {
|
||||
for(int c = 0; c < 4; ++c) {
|
||||
++seconds_[c];
|
||||
if(seconds_[c]) break;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current clock and data inputs to the clock.
|
||||
*/
|
||||
void set_input(bool clock, bool data) {
|
||||
/*
|
||||
Documented commands:
|
||||
|
||||
z0000001 Seconds register 0 (lowest order byte)
|
||||
z0000101 Seconds register 1
|
||||
z0001001 Seconds register 2
|
||||
z0001101 Seconds register 3
|
||||
00110001 Test register (write only)
|
||||
00110101 Write-protect register (write only)
|
||||
z010aa01 RAM addresses 0x10 - 0x13
|
||||
z1aaaa01 RAM addresses 0x00 – 0x0f
|
||||
|
||||
z = 1 => a read; z = 0 => a write.
|
||||
|
||||
The top bit of the write-protect register enables (0) or disables (1)
|
||||
writes to other locations.
|
||||
|
||||
All the documentation says about the test register is to set the top
|
||||
two bits to 0 for normal operation. Abnormal operation is undefined.
|
||||
|
||||
The data line is valid when the clock transitions to level 0.
|
||||
*/
|
||||
|
||||
if(clock && !previous_clock_) {
|
||||
// Shift into the command_ register, no matter what.
|
||||
command_ = uint16_t((command_ << 1) | (data ? 1 : 0));
|
||||
result_ <<= 1;
|
||||
|
||||
// Increment phase.
|
||||
++phase_;
|
||||
|
||||
// When phase hits 8, inspect the command.
|
||||
// If it's a read, prepare a result.
|
||||
if(phase_ == 8) {
|
||||
if(command_ & 0x80) {
|
||||
// A read.
|
||||
const auto address = (command_ >> 2) & 0x1f;
|
||||
|
||||
// Begin pessimistically.
|
||||
result_ = 0xff;
|
||||
|
||||
if(address < 4) {
|
||||
result_ = seconds_[address];
|
||||
} else if(address >= 0x10) {
|
||||
result_ = data_[address & 0xf];
|
||||
} else if(address >= 0x8 && address <= 0xb) {
|
||||
result_ = data_[0x10 + (address & 0x3)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If phase hits 16 and this was a read command,
|
||||
// just stop. If it was a write command, do the
|
||||
// actual write.
|
||||
if(phase_ == 16) {
|
||||
if(!(command_ & 0x8000)) {
|
||||
// A write.
|
||||
|
||||
const auto address = (command_ >> 10) & 0x1f;
|
||||
const uint8_t value = uint8_t(command_ & 0xff);
|
||||
|
||||
// First test: is this to the write-protect register?
|
||||
if(address == 0xd) {
|
||||
write_protect_ = value;
|
||||
}
|
||||
|
||||
// No other writing is permitted if the write protect
|
||||
// register won't allow it.
|
||||
if(!(write_protect_ & 0x80)) {
|
||||
if(address < 4) {
|
||||
seconds_[address] = value;
|
||||
} else if(address >= 0x10) {
|
||||
data_[address & 0xf] = value;
|
||||
} else if(address >= 0x8 && address <= 0xb) {
|
||||
data_[0x10 + (address & 0x3)] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A phase of 16 always ends the command, so reset here.
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
previous_clock_ = clock;
|
||||
}
|
||||
|
||||
/*!
|
||||
Reads the current data output level from the clock.
|
||||
*/
|
||||
bool get_data() {
|
||||
return !!(result_ & 0x80);
|
||||
}
|
||||
|
||||
/*!
|
||||
Announces that a serial command has been aborted.
|
||||
*/
|
||||
void abort() {
|
||||
result_ = 0;
|
||||
phase_ = 0;
|
||||
command_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t data_[0x14];
|
||||
uint8_t seconds_[4];
|
||||
uint8_t write_protect_;
|
||||
|
||||
int phase_ = 0;
|
||||
uint16_t command_;
|
||||
uint8_t result_ = 0;
|
||||
|
||||
bool previous_clock_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* RealTimeClock_hpp */
|
||||
201
Machines/Apple/Macintosh/Video.cpp
Normal file
201
Machines/Apple/Macintosh/Video.cpp
Normal file
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// Video.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 03/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Video.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
namespace {
|
||||
|
||||
const HalfCycles line_length(704);
|
||||
const int number_of_lines = 370;
|
||||
const HalfCycles frame_length(line_length * HalfCycles(number_of_lines));
|
||||
const int sync_start = 36;
|
||||
const int sync_end = 38;
|
||||
|
||||
}
|
||||
|
||||
// Re: CRT timings, see the Apple Guide to the Macintosh Hardware Family,
|
||||
// bottom of page 400:
|
||||
//
|
||||
// "For each scan line, 512 pixels are drawn on the screen ...
|
||||
// The horizontal blanking interval takes the time of an additional 192 pixels"
|
||||
//
|
||||
// And, at the top of 401:
|
||||
//
|
||||
// "The visible portion of a full-screen display consists of 342 horizontal scan lines...
|
||||
// During the vertical blanking interval, the turned-off beam ... traces out an additional 28 scan lines,"
|
||||
//
|
||||
Video::Video(uint16_t *ram, DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator) :
|
||||
audio_(audio),
|
||||
drive_speed_accumulator_(drive_speed_accumulator),
|
||||
crt_(704, 1, 370, Outputs::Display::ColourSpace::YIQ, 1, 1, 6, false, Outputs::Display::InputDataType::Luminance1),
|
||||
ram_(ram) {
|
||||
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));
|
||||
crt_.set_aspect_ratio(1.73f); // The Mac uses a non-standard scanning area.
|
||||
}
|
||||
|
||||
void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void Video::run_for(HalfCycles duration) {
|
||||
// Determine the current video and audio bases. These values don't appear to be latched, they apply immediately.
|
||||
const size_t video_base = (use_alternate_screen_buffer_ ? (0xffff2700 >> 1) : (0xffffa700 >> 1)) & ram_mask_;
|
||||
const size_t audio_base = (use_alternate_audio_buffer_ ? (0xffffa100 >> 1) : (0xfffffd00 >> 1)) & ram_mask_;
|
||||
|
||||
// The number of HalfCycles is literally the number of pixel clocks to move through,
|
||||
// since pixel output occurs at twice the processor clock. So divide by 16 to get
|
||||
// the number of fetches.
|
||||
while(duration > HalfCycles(0)) {
|
||||
const auto pixel_start = frame_position_ % line_length;
|
||||
const int line = (frame_position_ / line_length).as_int();
|
||||
|
||||
const auto cycles_left_in_line = std::min(line_length - pixel_start, duration);
|
||||
|
||||
// Line timing, entirely invented as I can find exactly zero words of documentation:
|
||||
//
|
||||
// First 342 lines:
|
||||
//
|
||||
// First 32 words = pixels;
|
||||
// next 5 words = right border;
|
||||
// next 2 words = sync level;
|
||||
// final 5 words = left border.
|
||||
//
|
||||
// Then 12 lines of border, 3 of sync, 11 more of border.
|
||||
|
||||
const int first_word = pixel_start.as_int() >> 4;
|
||||
const int final_word = (pixel_start + cycles_left_in_line).as_int() >> 4;
|
||||
|
||||
if(first_word != final_word) {
|
||||
if(line < 342) {
|
||||
// If there are any pixels left to output, do so.
|
||||
if(first_word < 32) {
|
||||
const int final_pixel_word = std::min(final_word, 32);
|
||||
|
||||
if(!first_word) {
|
||||
pixel_buffer_ = crt_.begin_data(512);
|
||||
}
|
||||
|
||||
if(pixel_buffer_) {
|
||||
for(int c = first_word; c < final_pixel_word; ++c) {
|
||||
uint16_t pixels = ram_[video_base + video_address_] ^ 0xffff;
|
||||
++video_address_;
|
||||
|
||||
pixel_buffer_[15] = pixels & 0x01;
|
||||
pixel_buffer_[14] = pixels & 0x02;
|
||||
pixel_buffer_[13] = pixels & 0x04;
|
||||
pixel_buffer_[12] = pixels & 0x08;
|
||||
pixel_buffer_[11] = pixels & 0x10;
|
||||
pixel_buffer_[10] = pixels & 0x20;
|
||||
pixel_buffer_[9] = pixels & 0x40;
|
||||
pixel_buffer_[8] = pixels & 0x80;
|
||||
|
||||
pixels >>= 8;
|
||||
pixel_buffer_[7] = pixels & 0x01;
|
||||
pixel_buffer_[6] = pixels & 0x02;
|
||||
pixel_buffer_[5] = pixels & 0x04;
|
||||
pixel_buffer_[4] = pixels & 0x08;
|
||||
pixel_buffer_[3] = pixels & 0x10;
|
||||
pixel_buffer_[2] = pixels & 0x20;
|
||||
pixel_buffer_[1] = pixels & 0x40;
|
||||
pixel_buffer_[0] = pixels & 0x80;
|
||||
|
||||
pixel_buffer_ += 16;
|
||||
}
|
||||
}
|
||||
|
||||
if(final_pixel_word == 32) {
|
||||
crt_.output_data(512);
|
||||
}
|
||||
}
|
||||
|
||||
if(first_word < sync_start && final_word >= sync_start) crt_.output_blank((sync_start - 32) * 16);
|
||||
if(first_word < sync_end && final_word >= sync_end) crt_.output_sync((sync_end - sync_start) * 16);
|
||||
if(final_word == 44) crt_.output_blank((44 - sync_end) * 16);
|
||||
} else if(final_word == 44) {
|
||||
if(line >= 353 && line < 356) {
|
||||
/* Output a sync line. */
|
||||
crt_.output_sync(sync_start * 16);
|
||||
crt_.output_blank((sync_end - sync_start) * 16);
|
||||
crt_.output_sync((44 - sync_end) * 16);
|
||||
} else {
|
||||
/* Output a blank line. */
|
||||
crt_.output_blank(sync_start * 16);
|
||||
crt_.output_sync((sync_end - sync_start) * 16);
|
||||
crt_.output_blank((44 - sync_end) * 16);
|
||||
}
|
||||
}
|
||||
|
||||
// Audio and disk fetches occur "just before video data".
|
||||
if(final_word == 44) {
|
||||
const uint16_t audio_word = ram_[audio_address_ + audio_base];
|
||||
++audio_address_;
|
||||
audio_.audio.post_sample(audio_word >> 8);
|
||||
drive_speed_accumulator_.post_sample(audio_word & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
duration -= cycles_left_in_line;
|
||||
frame_position_ = frame_position_ + cycles_left_in_line;
|
||||
if(frame_position_ == frame_length) {
|
||||
frame_position_ = HalfCycles(0);
|
||||
/*
|
||||
Video: $1A700 and the alternate buffer starts at $12700; for a 512K Macintosh, add $60000 to these numbers.
|
||||
*/
|
||||
video_address_ = 0;
|
||||
|
||||
/*
|
||||
"The main sound buffer is at $1FD00 in a 128K Macintosh, and the alternate buffer is at $1A100;
|
||||
for a 512K Macintosh, add $60000 to these values."
|
||||
*/
|
||||
audio_address_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Video::vsync() {
|
||||
const int line = (frame_position_ / line_length).as_int();
|
||||
return line >= 353 && line < 356;
|
||||
}
|
||||
|
||||
HalfCycles Video::get_next_sequence_point() {
|
||||
const int line = (frame_position_ / line_length).as_int();
|
||||
if(line >= 353 && line < 356) {
|
||||
// Currently in vsync, so get time until start of line 357,
|
||||
// when vsync will end.
|
||||
return HalfCycles(356) * line_length - frame_position_;
|
||||
} else {
|
||||
// Not currently in vsync, so get time until start of line 353.
|
||||
const auto start_of_vsync = HalfCycles(353) * line_length;
|
||||
if(frame_position_ < start_of_vsync)
|
||||
return start_of_vsync - frame_position_;
|
||||
else
|
||||
return start_of_vsync + HalfCycles(number_of_lines) * line_length - frame_position_;
|
||||
}
|
||||
}
|
||||
|
||||
bool Video::is_outputting(HalfCycles offset) {
|
||||
const auto offset_position = frame_position_ + offset % frame_length;
|
||||
const int column = (offset_position % line_length).as_int() >> 4;
|
||||
const int line = (offset_position / line_length).as_int();
|
||||
return line < 342 && column < 32;
|
||||
}
|
||||
|
||||
void Video::set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) {
|
||||
use_alternate_screen_buffer_ = use_alternate_screen_buffer;
|
||||
use_alternate_audio_buffer_ = use_alternate_audio_buffer;
|
||||
}
|
||||
|
||||
void Video::set_ram_mask(uint32_t mask) {
|
||||
ram_mask_ = mask;
|
||||
}
|
||||
94
Machines/Apple/Macintosh/Video.hpp
Normal file
94
Machines/Apple/Macintosh/Video.hpp
Normal file
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// Video.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 03/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Video_hpp
|
||||
#define Video_hpp
|
||||
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "DeferredAudio.hpp"
|
||||
#include "DriveSpeedAccumulator.hpp"
|
||||
|
||||
namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
/*!
|
||||
Models the 68000-era Macintosh video hardware, producing a 512x348 pixel image,
|
||||
within a total scanning area of 370 lines, at 352 cycles per line.
|
||||
|
||||
This class also collects audio and 400kb drive-speed data, forwarding those values.
|
||||
*/
|
||||
class Video {
|
||||
public:
|
||||
/*!
|
||||
Constructs an instance of @c Video sourcing its pixel data from @c ram and
|
||||
providing audio and drive-speed bytes to @c audio and @c drive_speed_accumulator.
|
||||
*/
|
||||
Video(uint16_t *ram, DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator);
|
||||
|
||||
/*!
|
||||
Sets the target device for video data.
|
||||
*/
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/*!
|
||||
Produces the next @c duration period of pixels.
|
||||
*/
|
||||
void run_for(HalfCycles duration);
|
||||
|
||||
/*!
|
||||
Sets whether the alternate screen and/or audio buffers should be used to source data.
|
||||
*/
|
||||
void set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer);
|
||||
|
||||
/*!
|
||||
Provides a mask indicating which parts of the generated video and audio/drive addresses are
|
||||
actually decoded, accessing *word-sized memory*; e.g. for a 128kb Macintosh this should be (1 << 16) - 1 = 0xffff.
|
||||
*/
|
||||
void set_ram_mask(uint32_t);
|
||||
|
||||
/*!
|
||||
@returns @c true if the video is currently outputting a vertical sync, @c false otherwise.
|
||||
*/
|
||||
bool vsync();
|
||||
|
||||
/*
|
||||
@returns @c true if in @c offset half cycles from now, the video will be outputting pixels;
|
||||
@c false otherwise.
|
||||
*/
|
||||
bool is_outputting(HalfCycles offset = HalfCycles(0));
|
||||
|
||||
/*!
|
||||
@returns the amount of time until there is next a transition on the
|
||||
vsync signal.
|
||||
*/
|
||||
HalfCycles get_next_sequence_point();
|
||||
|
||||
private:
|
||||
DeferredAudio &audio_;
|
||||
DriveSpeedAccumulator &drive_speed_accumulator_;
|
||||
|
||||
Outputs::CRT::CRT crt_;
|
||||
uint16_t *ram_ = nullptr;
|
||||
uint32_t ram_mask_ = 0;
|
||||
|
||||
HalfCycles frame_position_;
|
||||
|
||||
size_t video_address_ = 0;
|
||||
size_t audio_address_ = 0;
|
||||
|
||||
uint8_t *pixel_buffer_ = nullptr;
|
||||
|
||||
bool use_alternate_screen_buffer_ = false;
|
||||
bool use_alternate_audio_buffer_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Video_hpp */
|
||||
@@ -55,13 +55,13 @@ class Bus {
|
||||
// video backlog accumulation counter
|
||||
Cycles cycles_since_video_update_;
|
||||
inline void update_video() {
|
||||
tia_.run_for(cycles_since_video_update_.flush());
|
||||
tia_.run_for(cycles_since_video_update_.flush<Cycles>());
|
||||
}
|
||||
|
||||
// RIOT backlog accumulation counter
|
||||
Cycles cycles_since_6532_update_;
|
||||
inline void update_6532() {
|
||||
mos6532_.run_for(cycles_since_6532_update_.flush());
|
||||
mos6532_.run_for(cycles_since_6532_update_.flush<Cycles>());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
#include "../CRTMachine.hpp"
|
||||
#include "../JoystickMachine.hpp"
|
||||
|
||||
#include "../../Configurable/StandardOptions.hpp"
|
||||
#include "../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include "../../Outputs/Speaker/Implementation/CompoundSource.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
@@ -31,6 +33,12 @@ const int sn76489_divider = 2;
|
||||
namespace Coleco {
|
||||
namespace Vision {
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
return Configurable::standard_options(
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplaySVideo | Configurable::DisplayCompositeColour)
|
||||
);
|
||||
}
|
||||
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick() :
|
||||
@@ -107,6 +115,7 @@ class ConcreteMachine:
|
||||
public Machine,
|
||||
public CPU::Z80::BusHandler,
|
||||
public CRTMachine::Machine,
|
||||
public Configurable::Device,
|
||||
public JoystickMachine::Machine {
|
||||
|
||||
public:
|
||||
@@ -123,8 +132,7 @@ class ConcreteMachine:
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
|
||||
const auto roms = rom_fetcher(
|
||||
"ColecoVision",
|
||||
{ "coleco.rom" });
|
||||
{ {"ColecoVision", "the ColecoVision BIOS", "coleco.rom", 8*1024, 0x3aa93ef3} });
|
||||
|
||||
if(!roms[0]) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
@@ -162,7 +170,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
// ColecoVisions have composite output only.
|
||||
vdp_.set_display_type(Outputs::Display::DisplayType::CompositeColour);
|
||||
vdp_->set_display_type(Outputs::Display::DisplayType::CompositeColour);
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
@@ -174,11 +182,11 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
vdp_.set_scan_target(scan_target);
|
||||
vdp_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
vdp_.set_display_type(display_type);
|
||||
vdp_->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
@@ -203,7 +211,7 @@ class ConcreteMachine:
|
||||
}
|
||||
const HalfCycles length = cycle.length + penalty;
|
||||
|
||||
time_since_vdp_update_ += length;
|
||||
vdp_ += length;
|
||||
time_since_sn76489_update_ += length;
|
||||
|
||||
// Act only if necessary.
|
||||
@@ -248,10 +256,9 @@ class ConcreteMachine:
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
switch((address >> 5) & 7) {
|
||||
case 5:
|
||||
update_video();
|
||||
*cycle.value = vdp_.get_register(address);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_.get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_.get_time_until_interrupt();
|
||||
*cycle.value = vdp_->get_register(address);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 7: {
|
||||
@@ -292,10 +299,9 @@ class ConcreteMachine:
|
||||
break;
|
||||
|
||||
case 5:
|
||||
update_video();
|
||||
vdp_.set_register(address, *cycle.value);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_.get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_.get_time_until_interrupt();
|
||||
vdp_->set_register(address, *cycle.value);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 7:
|
||||
@@ -347,7 +353,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
void flush() {
|
||||
update_video();
|
||||
vdp_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
@@ -357,6 +363,30 @@ class ConcreteMachine:
|
||||
return confidence_counter_.get_confidence();
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
return Coleco::Vision::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
Configurable::Display display;
|
||||
if(Configurable::get_display(selections_by_option, display)) {
|
||||
set_video_signal_configurable(display);
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::SVideo);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
private:
|
||||
inline void page_megacart(uint16_t address) {
|
||||
const std::size_t selected_start = (static_cast<std::size_t>(address&63) << 14) % cartridge_.size();
|
||||
@@ -365,12 +395,9 @@ class ConcreteMachine:
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(sn76489_divider)));
|
||||
}
|
||||
inline void update_video() {
|
||||
vdp_.run_for(time_since_vdp_update_.flush());
|
||||
}
|
||||
|
||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||
TI::TMS::TMS9918 vdp_;
|
||||
JustInTimeActor<TI::TMS::TMS9918, HalfCycles> vdp_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
@@ -393,7 +420,6 @@ class ConcreteMachine:
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
bool joysticks_in_keypad_mode_ = false;
|
||||
|
||||
HalfCycles time_since_vdp_update_;
|
||||
HalfCycles time_since_sn76489_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
|
||||
|
||||
@@ -9,12 +9,15 @@
|
||||
#ifndef ColecoVision_hpp
|
||||
#define ColecoVision_hpp
|
||||
|
||||
#include "../../Configurable/Configurable.hpp"
|
||||
#include "../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../ROMMachine.hpp"
|
||||
|
||||
namespace Coleco {
|
||||
namespace Vision {
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options();
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
|
||||
@@ -39,13 +39,20 @@ MachineBase::MachineBase(Personality personality, const ROMMachine::ROMFetcher &
|
||||
// attach the only drive there is
|
||||
set_drive(drive_);
|
||||
|
||||
std::string rom_name;
|
||||
std::string device_name;
|
||||
uint32_t crc = 0;
|
||||
switch(personality) {
|
||||
case Personality::C1540: rom_name = "1540.bin"; break;
|
||||
case Personality::C1541: rom_name = "1541.bin"; break;
|
||||
case Personality::C1540:
|
||||
device_name = "1540";
|
||||
crc = 0x718d42b1;
|
||||
break;
|
||||
case Personality::C1541:
|
||||
device_name = "1541";
|
||||
crc = 0xfb760019;
|
||||
break;
|
||||
}
|
||||
|
||||
auto roms = rom_fetcher("Commodore1540", {rom_name});
|
||||
auto roms = rom_fetcher({ {"Commodore1540", "the " + device_name + " ROM", device_name + ".bin", 16*1024, crc} });
|
||||
if(!roms[0]) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
@@ -323,31 +323,34 @@ class ConcreteMachine:
|
||||
// install a joystick
|
||||
joysticks_.emplace_back(new Joystick(*user_port_via_port_handler_, *keyboard_via_port_handler_));
|
||||
|
||||
std::vector<std::string> rom_names = { "basic.bin" };
|
||||
const std::string machine_name = "Vic20";
|
||||
std::vector<ROMMachine::ROM> rom_names = {
|
||||
{machine_name, "the VIC-20 BASIC ROM", "basic.bin", 8*1024, 0xdb4c43c1}
|
||||
};
|
||||
switch(target.region) {
|
||||
default:
|
||||
rom_names.push_back("characters-english.bin");
|
||||
rom_names.push_back("kernel-pal.bin");
|
||||
rom_names.emplace_back(machine_name, "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6);
|
||||
rom_names.emplace_back(machine_name, "the English-language PAL VIC-20 kernel ROM", "kernel-pal.bin", 8*1024, 0x4be07cb4);
|
||||
break;
|
||||
case Analyser::Static::Commodore::Target::Region::American:
|
||||
rom_names.push_back("characters-english.bin");
|
||||
rom_names.push_back("kernel-ntsc.bin");
|
||||
rom_names.emplace_back(machine_name, "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6);
|
||||
rom_names.emplace_back(machine_name, "the English-language NTSC VIC-20 kernel ROM", "kernel-ntsc.bin", 8*1024, 0xe5e7c174);
|
||||
break;
|
||||
case Analyser::Static::Commodore::Target::Region::Danish:
|
||||
rom_names.push_back("characters-danish.bin");
|
||||
rom_names.push_back("kernel-danish.bin");
|
||||
rom_names.emplace_back(machine_name, "the Danish VIC-20 character ROM", "characters-danish.bin", 4*1024, 0x7fc11454);
|
||||
rom_names.emplace_back(machine_name, "the Danish VIC-20 kernel ROM", "kernel-danish.bin", 8*1024, 0x02adaf16);
|
||||
break;
|
||||
case Analyser::Static::Commodore::Target::Region::Japanese:
|
||||
rom_names.push_back("characters-japanese.bin");
|
||||
rom_names.push_back("kernel-japanese.bin");
|
||||
rom_names.emplace_back(machine_name, "the Japanese VIC-20 character ROM", "characters-japanese.bin", 4*1024, 0xfcfd8a4b);
|
||||
rom_names.emplace_back(machine_name, "the Japanese VIC-20 kernel ROM", "kernel-japanese.bin", 8*1024, 0x336900d7);
|
||||
break;
|
||||
case Analyser::Static::Commodore::Target::Region::Swedish:
|
||||
rom_names.push_back("characters-swedish.bin");
|
||||
rom_names.push_back("kernel-japanese.bin");
|
||||
rom_names.emplace_back(machine_name, "the Swedish VIC-20 character ROM", "characters-swedish.bin", 4*1024, 0xd808551d);
|
||||
rom_names.emplace_back(machine_name, "the Swedish VIC-20 kernel ROM", "kernel-swedish.bin", 8*1024, 0xb2a60662);
|
||||
break;
|
||||
}
|
||||
|
||||
const auto roms = rom_fetcher("Vic20", rom_names);
|
||||
const auto roms = rom_fetcher(rom_names);
|
||||
|
||||
for(const auto &rom: roms) {
|
||||
if(!rom) {
|
||||
@@ -699,7 +702,7 @@ class ConcreteMachine:
|
||||
|
||||
private:
|
||||
void update_video() {
|
||||
mos6560_.run_for(cycles_since_mos6560_update_.flush());
|
||||
mos6560_.run_for(cycles_since_mos6560_update_.flush<Cycles>());
|
||||
}
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
|
||||
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
|
||||
#include "../Configurable/Configurable.hpp"
|
||||
#include "../Activity/Source.hpp"
|
||||
#include "MediaTarget.hpp"
|
||||
|
||||
#include "CRTMachine.hpp"
|
||||
#include "JoystickMachine.hpp"
|
||||
#include "KeyboardMachine.hpp"
|
||||
#include "MediaTarget.hpp"
|
||||
#include "MouseMachine.hpp"
|
||||
|
||||
#include "Utility/Typer.hpp"
|
||||
|
||||
namespace Machine {
|
||||
@@ -31,6 +34,7 @@ struct DynamicMachine {
|
||||
virtual CRTMachine::Machine *crt_machine() = 0;
|
||||
virtual JoystickMachine::Machine *joystick_machine() = 0;
|
||||
virtual KeyboardMachine::Machine *keyboard_machine() = 0;
|
||||
virtual MouseMachine::Machine *mouse_machine() = 0;
|
||||
virtual MediaTarget::Machine *media_target() = 0;
|
||||
|
||||
/*!
|
||||
|
||||
@@ -62,17 +62,22 @@ class ConcreteMachine:
|
||||
set_clock_rate(2000000);
|
||||
|
||||
speaker_.set_input_rate(2000000 / SoundGenerator::clock_rate_divider);
|
||||
speaker_.set_high_frequency_cutoff(7000);
|
||||
|
||||
std::vector<std::string> rom_names = {"basic.rom", "os.rom"};
|
||||
const std::string machine_name = "Electron";
|
||||
std::vector<ROMMachine::ROM> required_roms = {
|
||||
{machine_name, "the Acorn BASIC II ROM", "basic.rom", 16*1024, 0x79434781},
|
||||
{machine_name, "the Electron MOS ROM", "os.rom", 16*1024, 0xbf63fb1f}
|
||||
};
|
||||
if(target.has_adfs) {
|
||||
rom_names.push_back("ADFS-E00_1.rom");
|
||||
rom_names.push_back("ADFS-E00_2.rom");
|
||||
required_roms.emplace_back(machine_name, "the E00 ADFS ROM, first slot", "ADFS-E00_1.rom", 16*1024, 0x51523993);
|
||||
required_roms.emplace_back(machine_name, "the E00 ADFS ROM, second slot", "ADFS-E00_2.rom", 16*1024, 0x8d17de0e);
|
||||
}
|
||||
const size_t dfs_rom_position = rom_names.size();
|
||||
const size_t dfs_rom_position = required_roms.size();
|
||||
if(target.has_dfs) {
|
||||
rom_names.push_back("DFS-1770-2.20.rom");
|
||||
required_roms.emplace_back(machine_name, "the 1770 DFS ROM", "DFS-1770-2.20.rom", 16*1024, 0xf3dc9bc5);
|
||||
}
|
||||
const auto roms = rom_fetcher("Electron", rom_names);
|
||||
const auto roms = rom_fetcher(required_roms);
|
||||
|
||||
for(const auto &rom: roms) {
|
||||
if(!rom) {
|
||||
@@ -505,7 +510,7 @@ class ConcreteMachine:
|
||||
// MARK: - Work deferral updates.
|
||||
inline void update_display() {
|
||||
if(cycles_since_display_update_ > 0) {
|
||||
video_output_.run_for(cycles_since_display_update_.flush());
|
||||
video_output_.run_for(cycles_since_display_update_.flush<Cycles>());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ enum Key: uint16_t {
|
||||
};
|
||||
|
||||
struct KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key);
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) override;
|
||||
};
|
||||
|
||||
struct CharacterMapper: public ::Utility::CharacterMapper {
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
|
||||
#include "../../Configurable/StandardOptions.hpp"
|
||||
#include "../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include "../../Analyser/Static/MSX/Target.hpp"
|
||||
|
||||
@@ -173,33 +174,37 @@ class ConcreteMachine:
|
||||
mixer_.set_relative_volumes({0.5f, 0.1f, 0.4f});
|
||||
|
||||
// Install the proper TV standard and select an ideal BIOS name.
|
||||
std::vector<std::string> rom_names = {"msx.rom"};
|
||||
const std::string machine_name = "MSX";
|
||||
std::vector<ROMMachine::ROM> required_roms = {
|
||||
{machine_name, "any MSX BIOS", "msx.rom", 32*1024, 0x94ee12f3}
|
||||
};
|
||||
|
||||
bool is_ntsc = true;
|
||||
uint8_t character_generator = 1; /* 0 = Japan, 1 = USA, etc, 2 = USSR */
|
||||
uint8_t date_format = 1; /* 0 = Y/M/D, 1 = M/D/Y, 2 = D/M/Y */
|
||||
uint8_t keyboard = 1; /* 0 = Japan, 1 = USA, 2 = France, 3 = UK, 4 = Germany, 5 = USSR, 6 = Spain */
|
||||
|
||||
// TODO: CRCs below are incomplete, at best.
|
||||
switch(target.region) {
|
||||
case Target::Region::Japan:
|
||||
rom_names.push_back("msx-japanese.rom");
|
||||
vdp_.set_tv_standard(TI::TMS::TVStandard::NTSC);
|
||||
required_roms.emplace_back(machine_name, "a Japanese MSX BIOS", "msx-japanese.rom", 32*1024, 0xee229390);
|
||||
vdp_->set_tv_standard(TI::TMS::TVStandard::NTSC);
|
||||
|
||||
is_ntsc = true;
|
||||
character_generator = 0;
|
||||
date_format = 0;
|
||||
break;
|
||||
case Target::Region::USA:
|
||||
rom_names.push_back("msx-american.rom");
|
||||
vdp_.set_tv_standard(TI::TMS::TVStandard::NTSC);
|
||||
required_roms.emplace_back(machine_name, "an American MSX BIOS", "msx-american.rom", 32*1024, 0);
|
||||
vdp_->set_tv_standard(TI::TMS::TVStandard::NTSC);
|
||||
|
||||
is_ntsc = true;
|
||||
character_generator = 1;
|
||||
date_format = 1;
|
||||
break;
|
||||
case Target::Region::Europe:
|
||||
rom_names.push_back("msx-european.rom");
|
||||
vdp_.set_tv_standard(TI::TMS::TVStandard::PAL);
|
||||
required_roms.emplace_back(machine_name, "a European MSX BIOS", "msx-european.rom", 32*1024, 0);
|
||||
vdp_->set_tv_standard(TI::TMS::TVStandard::PAL);
|
||||
|
||||
is_ntsc = false;
|
||||
character_generator = 1;
|
||||
@@ -209,10 +214,12 @@ class ConcreteMachine:
|
||||
|
||||
// Fetch the necessary ROMs; try the region-specific ROM first,
|
||||
// but failing that fall back on patching the main one.
|
||||
size_t disk_index = 0;
|
||||
if(target.has_disk_drive) {
|
||||
rom_names.push_back("disk.rom");
|
||||
disk_index = required_roms.size();
|
||||
required_roms.emplace_back(machine_name, "the MSX-DOS ROM", "disk.rom", 16*1024, 0x721f61df);
|
||||
}
|
||||
const auto roms = rom_fetcher("MSX", rom_names);
|
||||
const auto roms = rom_fetcher(required_roms);
|
||||
|
||||
if((!roms[0] && !roms[1]) || (target.has_disk_drive && !roms[2])) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
@@ -227,7 +234,6 @@ class ConcreteMachine:
|
||||
memory_slots_[0].source = std::move(*roms[0]);
|
||||
memory_slots_[0].source.resize(32768);
|
||||
|
||||
|
||||
memory_slots_[0].source[0x2b] = uint8_t(
|
||||
(is_ntsc ? 0x00 : 0x80) |
|
||||
(date_format << 4) |
|
||||
@@ -252,7 +258,7 @@ class ConcreteMachine:
|
||||
// Add a disk cartridge if any disks were supplied.
|
||||
if(target.has_disk_drive) {
|
||||
memory_slots_[2].set_handler(new DiskROM(memory_slots_[2].source));
|
||||
memory_slots_[2].source = std::move(*roms[1]);
|
||||
memory_slots_[2].source = std::move(*roms[disk_index]);
|
||||
memory_slots_[2].source.resize(16384);
|
||||
|
||||
map(2, 0, 0x4000, 0x2000);
|
||||
@@ -273,11 +279,11 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
vdp_.set_scan_target(scan_target);
|
||||
vdp_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
vdp_.set_display_type(display_type);
|
||||
vdp_->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
@@ -406,7 +412,7 @@ class ConcreteMachine:
|
||||
// but otherwise runs without pause.
|
||||
const HalfCycles addition((cycle.operation == CPU::Z80::PartialMachineCycle::ReadOpcode) ? 2 : 0);
|
||||
const HalfCycles total_length = addition + cycle.length;
|
||||
time_since_vdp_update_ += total_length;
|
||||
vdp_ += total_length;
|
||||
time_since_ay_update_ += total_length;
|
||||
memory_slots_[0].cycles_since_update += total_length;
|
||||
memory_slots_[1].cycles_since_update += total_length;
|
||||
@@ -484,7 +490,7 @@ class ConcreteMachine:
|
||||
*cycle.value = read_pointers_[address >> 13][address & 8191];
|
||||
} else {
|
||||
int slot_hit = (paged_memory_ >> ((address >> 14) * 2)) & 3;
|
||||
memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush());
|
||||
memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush<HalfCycles>());
|
||||
*cycle.value = memory_slots_[slot_hit].handler->read(address);
|
||||
}
|
||||
break;
|
||||
@@ -495,7 +501,7 @@ class ConcreteMachine:
|
||||
int slot_hit = (paged_memory_ >> ((address >> 14) * 2)) & 3;
|
||||
if(memory_slots_[slot_hit].handler) {
|
||||
update_audio();
|
||||
memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush());
|
||||
memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush<HalfCycles>());
|
||||
memory_slots_[slot_hit].handler->write(address, *cycle.value, read_pointers_[pc_address_ >> 13] != memory_slots_[0].read_pointers[pc_address_ >> 13]);
|
||||
}
|
||||
} break;
|
||||
@@ -503,10 +509,9 @@ class ConcreteMachine:
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
switch(address & 0xff) {
|
||||
case 0x98: case 0x99:
|
||||
vdp_.run_for(time_since_vdp_update_.flush());
|
||||
*cycle.value = vdp_.get_register(address);
|
||||
z80_.set_interrupt_line(vdp_.get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_.get_time_until_interrupt();
|
||||
*cycle.value = vdp_->get_register(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 0xa2:
|
||||
@@ -531,10 +536,9 @@ class ConcreteMachine:
|
||||
const int port = address & 0xff;
|
||||
switch(port) {
|
||||
case 0x98: case 0x99:
|
||||
vdp_.run_for(time_since_vdp_update_.flush());
|
||||
vdp_.set_register(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_.get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_.get_time_until_interrupt();
|
||||
vdp_->set_register(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 0xa0: case 0xa1:
|
||||
@@ -607,7 +611,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
void flush() {
|
||||
vdp_.run_for(time_since_vdp_update_.flush());
|
||||
vdp_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
@@ -748,7 +752,7 @@ class ConcreteMachine:
|
||||
};
|
||||
|
||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||
TI::TMS::TMS9918 vdp_;
|
||||
JustInTimeActor<TI::TMS::TMS9918, HalfCycles> vdp_;
|
||||
Intel::i8255::i8255<i8255PortHandler> i8255_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
@@ -792,7 +796,6 @@ class ConcreteMachine:
|
||||
uint8_t scratch_[8192];
|
||||
uint8_t unpopulated_[8192];
|
||||
|
||||
HalfCycles time_since_vdp_update_;
|
||||
HalfCycles time_since_ay_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "../KeyboardMachine.hpp"
|
||||
|
||||
#include "../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../Outputs/Log.hpp"
|
||||
@@ -132,7 +133,12 @@ class ConcreteMachine:
|
||||
|
||||
// Load the BIOS if relevant.
|
||||
if(has_bios()) {
|
||||
const auto roms = rom_fetcher("MasterSystem", {"bios.sms"});
|
||||
// TODO: there's probably a million other versions of the Master System BIOS; try to build a
|
||||
// CRC32 catalogue of those. So far:
|
||||
//
|
||||
// 0072ed54 = US/European BIOS 1.3
|
||||
// 48d44a13 = Japanese BIOS 2.1
|
||||
const auto roms = rom_fetcher({ {"MasterSystem", "the Master System BIOS", "bios.sms", 8*1024, { 0x0072ed54, 0x48d44a13 } } });
|
||||
if(!roms[0]) {
|
||||
// No BIOS found; attempt to boot as though it has already disabled itself.
|
||||
memory_control_ |= 0x08;
|
||||
@@ -163,16 +169,16 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
vdp_.set_tv_standard(
|
||||
vdp_->set_tv_standard(
|
||||
(region_ == Target::Region::Europe) ?
|
||||
TI::TMS::TVStandard::PAL : TI::TMS::TVStandard::NTSC);
|
||||
time_until_debounce_ = vdp_.get_time_until_line(-1);
|
||||
time_until_debounce_ = vdp_->get_time_until_line(-1);
|
||||
|
||||
vdp_.set_scan_target(scan_target);
|
||||
vdp_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
vdp_.set_display_type(display_type);
|
||||
vdp_->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
@@ -184,7 +190,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
time_since_vdp_update_ += cycle.length;
|
||||
vdp_ += cycle.length;
|
||||
time_since_sn76489_update_ += cycle.length;
|
||||
|
||||
if(cycle.is_terminal()) {
|
||||
@@ -228,17 +234,15 @@ class ConcreteMachine:
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
case 0x40:
|
||||
update_video();
|
||||
*cycle.value = vdp_.get_current_line();
|
||||
*cycle.value = vdp_->get_current_line();
|
||||
break;
|
||||
case 0x41:
|
||||
*cycle.value = vdp_.get_latched_horizontal_counter();
|
||||
*cycle.value = vdp_.last_valid()->get_latched_horizontal_counter();
|
||||
break;
|
||||
case 0x80: case 0x81:
|
||||
update_video();
|
||||
*cycle.value = vdp_.get_register(address);
|
||||
z80_.set_interrupt_line(vdp_.get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_.get_time_until_interrupt();
|
||||
*cycle.value = vdp_->get_register(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
case 0xc0: {
|
||||
Joystick *const joypad1 = static_cast<Joystick *>(joysticks_[0].get());
|
||||
@@ -279,8 +283,7 @@ class ConcreteMachine:
|
||||
|
||||
// Latch if either TH has newly gone to 1.
|
||||
if((new_ths^previous_ths)&new_ths) {
|
||||
update_video();
|
||||
vdp_.latch_horizontal_counter();
|
||||
vdp_->latch_horizontal_counter();
|
||||
}
|
||||
} break;
|
||||
case 0x40: case 0x41:
|
||||
@@ -288,10 +291,9 @@ class ConcreteMachine:
|
||||
sn76489_.set_register(*cycle.value);
|
||||
break;
|
||||
case 0x80: case 0x81:
|
||||
update_video();
|
||||
vdp_.set_register(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_.get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_.get_time_until_interrupt();
|
||||
vdp_->set_register(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
case 0xc0:
|
||||
LOG("TODO: [output] I/O port A/N; " << int(*cycle.value));
|
||||
@@ -326,15 +328,14 @@ class ConcreteMachine:
|
||||
time_until_debounce_ -= cycle.length;
|
||||
if(time_until_debounce_ <= HalfCycles(0)) {
|
||||
z80_.set_non_maskable_interrupt_line(pause_is_pressed_);
|
||||
update_video();
|
||||
time_until_debounce_ = vdp_.get_time_until_line(-1);
|
||||
time_until_debounce_ = vdp_->get_time_until_line(-1);
|
||||
}
|
||||
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
void flush() {
|
||||
update_video();
|
||||
vdp_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
@@ -407,16 +408,13 @@ class ConcreteMachine:
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(sn76489_divider)));
|
||||
}
|
||||
inline void update_video() {
|
||||
vdp_.run_for(time_since_vdp_update_.flush());
|
||||
}
|
||||
|
||||
using Target = Analyser::Static::Sega::Target;
|
||||
Target::Model model_;
|
||||
Target::Region region_;
|
||||
Target::PagingScheme paging_scheme_;
|
||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||
TI::TMS::TMS9918 vdp_;
|
||||
JustInTimeActor<TI::TMS::TMS9918, HalfCycles> vdp_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
@@ -426,7 +424,6 @@ class ConcreteMachine:
|
||||
Inputs::Keyboard keyboard_;
|
||||
bool reset_is_pressed_ = false, pause_is_pressed_ = false;
|
||||
|
||||
HalfCycles time_since_vdp_update_;
|
||||
HalfCycles time_since_sn76489_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
HalfCycles time_until_debounce_;
|
||||
|
||||
23
Machines/MouseMachine.hpp
Normal file
23
Machines/MouseMachine.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// MouseMachine.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 11/06/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef MouseMachine_hpp
|
||||
#define MouseMachine_hpp
|
||||
|
||||
#include "../Inputs/Mouse.hpp"
|
||||
|
||||
namespace MouseMachine {
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
virtual Inputs::Mouse &get_mouse() = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* MouseMachine_hpp */
|
||||
@@ -170,7 +170,7 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
/*!
|
||||
Advances time. This class manages the AY's concept of time to permit updating-on-demand.
|
||||
*/
|
||||
inline void run_for(const Cycles cycles) {
|
||||
inline void run_for(const HalfCycles cycles) {
|
||||
cycles_since_ay_update_ += cycles;
|
||||
}
|
||||
|
||||
@@ -182,11 +182,11 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
|
||||
private:
|
||||
void update_ay() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_ay_update_.flush());
|
||||
speaker_.run_for(audio_queue_, cycles_since_ay_update_.flush<Cycles>());
|
||||
}
|
||||
bool ay_bdir_ = false;
|
||||
bool ay_bc1_ = false;
|
||||
Cycles cycles_since_ay_update_;
|
||||
HalfCycles cycles_since_ay_update_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
GI::AY38910::AY38910 &ay8910_;
|
||||
@@ -228,19 +228,34 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
diskii_.set_clocking_hint_observer(this);
|
||||
}
|
||||
|
||||
std::vector<std::string> rom_names = {"colour.rom"};
|
||||
const std::string machine_name = "Oric";
|
||||
std::vector<ROMMachine::ROM> rom_names = { {machine_name, "the Oric colour ROM", "colour.rom", 128, 0xd50fca65} };
|
||||
switch(target.rom) {
|
||||
case Analyser::Static::Oric::Target::ROM::BASIC10: rom_names.push_back("basic10.rom"); break;
|
||||
case Analyser::Static::Oric::Target::ROM::BASIC11: rom_names.push_back("basic11.rom"); break;
|
||||
case Analyser::Static::Oric::Target::ROM::Pravetz: rom_names.push_back("pravetz.rom"); break;
|
||||
case Analyser::Static::Oric::Target::ROM::BASIC10:
|
||||
rom_names.emplace_back(machine_name, "Oric BASIC 1.0", "basic10.rom", 16*1024, 0xf18710b4);
|
||||
break;
|
||||
case Analyser::Static::Oric::Target::ROM::BASIC11:
|
||||
rom_names.emplace_back(machine_name, "Oric BASIC 1.1", "basic11.rom", 16*1024, 0xc3a92bef);
|
||||
break;
|
||||
case Analyser::Static::Oric::Target::ROM::Pravetz:
|
||||
rom_names.emplace_back(machine_name, "Pravetz BASIC", "pravetz.rom", 16*1024, 0x58079502);
|
||||
break;
|
||||
}
|
||||
size_t diskii_state_machine_index = 0;
|
||||
switch(disk_interface) {
|
||||
default: break;
|
||||
case Analyser::Static::Oric::Target::DiskInterface::Microdisc: rom_names.push_back("microdisc.rom"); break;
|
||||
case Analyser::Static::Oric::Target::DiskInterface::Pravetz: rom_names.push_back("8dos.rom"); break;
|
||||
case Analyser::Static::Oric::Target::DiskInterface::Microdisc:
|
||||
rom_names.emplace_back(machine_name, "the ORIC Microdisc ROM", "microdisc.rom", 8*1024, 0xa9664a9c);
|
||||
break;
|
||||
case Analyser::Static::Oric::Target::DiskInterface::Pravetz:
|
||||
rom_names.emplace_back(machine_name, "the 8DOS boot ROM", "8dos.rom", 512, 0x49a74c06);
|
||||
// These ROM details are coupled with those in the DiskIICard.
|
||||
diskii_state_machine_index = rom_names.size();
|
||||
rom_names.push_back({"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 }});
|
||||
break;
|
||||
}
|
||||
|
||||
const auto roms = rom_fetcher("Oric", rom_names);
|
||||
const auto roms = rom_fetcher(rom_names);
|
||||
|
||||
for(std::size_t index = 0; index < roms.size(); ++index) {
|
||||
if(!roms[index]) {
|
||||
@@ -261,11 +276,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
pravetz_rom_ = std::move(*roms[2]);
|
||||
pravetz_rom_.resize(512);
|
||||
|
||||
auto state_machine_rom = rom_fetcher("DiskII", {"state-machine-16.rom"});
|
||||
if(!state_machine_rom[0]) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
diskii_.set_state_machine(*state_machine_rom[0]);
|
||||
diskii_.set_state_machine(*roms[diskii_state_machine_index]);
|
||||
} break;
|
||||
}
|
||||
|
||||
@@ -434,7 +445,6 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
}
|
||||
|
||||
via_.run_for(Cycles(1));
|
||||
via_port_handler_.run_for(Cycles(1));
|
||||
tape_player_.run_for(Cycles(1));
|
||||
switch(disk_interface) {
|
||||
default: break;
|
||||
@@ -456,7 +466,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
|
||||
forceinline void flush() {
|
||||
update_video();
|
||||
via_port_handler_.flush();
|
||||
via_.flush();
|
||||
flush_diskii();
|
||||
}
|
||||
|
||||
@@ -575,7 +585,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
uint8_t ram_[65536];
|
||||
Cycles cycles_since_video_update_;
|
||||
inline void update_video() {
|
||||
video_output_.run_for(cycles_since_video_update_.flush());
|
||||
video_output_.run_for(cycles_since_video_update_.flush<Cycles>());
|
||||
}
|
||||
|
||||
// ROM bookkeeping
|
||||
@@ -607,7 +617,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
Apple::DiskII diskii_;
|
||||
Cycles cycles_since_diskii_update_;
|
||||
void flush_diskii() {
|
||||
diskii_.run_for(cycles_since_diskii_update_.flush());
|
||||
diskii_.run_for(cycles_since_diskii_update_.flush<Cycles>());
|
||||
}
|
||||
std::vector<uint8_t> pravetz_rom_;
|
||||
std::size_t pravetz_rom_base_pointer_ = 0;
|
||||
|
||||
@@ -16,15 +16,41 @@
|
||||
|
||||
namespace ROMMachine {
|
||||
|
||||
/*!
|
||||
Describes a ROM image; this term is used in this emulator strictly in the sense of firmware —
|
||||
system software that is an inherent part of a machine.
|
||||
*/
|
||||
struct ROM {
|
||||
/// The machine with which this ROM is associated, in a form that is safe for using as
|
||||
/// part of a file name.
|
||||
std::string machine_name;
|
||||
/// A descriptive name for this ROM, suitable for use in a bullet-point list, a bracket
|
||||
/// clause, etc, e.g. "the Electron MOS 1.0".
|
||||
std::string descriptive_name;
|
||||
/// An idiomatic file name for this ROM, e.g. "os10.rom".
|
||||
std::string file_name;
|
||||
/// The expected size of this ROM in bytes, e.g. 32768.
|
||||
size_t size = 0;
|
||||
/// CRC32s for all known acceptable copies of this ROM; intended to allow a host platform
|
||||
/// to test user-provided ROMs of unknown provenance. **Not** intended to be used
|
||||
/// to exclude ROMs where the user's intent is otherwise clear.
|
||||
std::vector<uint32_t> crc32s;
|
||||
|
||||
ROM(std::string machine_name, std::string descriptive_name, std::string file_name, size_t size, uint32_t crc32) :
|
||||
machine_name(machine_name), descriptive_name(descriptive_name), file_name(file_name), size(size), crc32s({crc32}) {}
|
||||
ROM(std::string machine_name, std::string descriptive_name, std::string file_name, size_t size, std::initializer_list<uint32_t> crc32s) :
|
||||
machine_name(machine_name), descriptive_name(descriptive_name), file_name(file_name), size(size), crc32s(crc32s) {}
|
||||
};
|
||||
|
||||
/*!
|
||||
Defines the signature for a function that must be supplied by the host environment in order to give machines
|
||||
a route for fetching any system ROMs they might need.
|
||||
|
||||
The caller will supply the idiomatic name of the machine plus a vector of the names of ROM files that it expects
|
||||
to be present. The recevier should return a vector of unique_ptrs that either contain the contents of the
|
||||
ROM from @c names that corresponds by index, or else are the nullptr
|
||||
The caller will supply a vector of the names of ROM files that it would like to inspect. The recevier should
|
||||
return a vector of unique_ptrs that either contain the contents of the ROM from @c names that corresponds by
|
||||
index, or else are @c nullptr.
|
||||
*/
|
||||
typedef std::function<std::vector<std::unique_ptr<std::vector<uint8_t>>>(const std::string &machine, const std::vector<std::string> &names)> ROMFetcher;
|
||||
typedef std::function<std::vector<std::unique_ptr<std::vector<uint8_t>>>(const std::vector<ROM> &roms)> ROMFetcher;
|
||||
|
||||
enum class Error {
|
||||
MissingROMs
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
#include "MachineForTarget.hpp"
|
||||
|
||||
#include "../AmstradCPC/AmstradCPC.hpp"
|
||||
#include "../AppleII/AppleII.hpp"
|
||||
#include "../Apple/AppleII/AppleII.hpp"
|
||||
#include "../Apple/Macintosh/Macintosh.hpp"
|
||||
#include "../Atari2600/Atari2600.hpp"
|
||||
#include "../ColecoVision/ColecoVision.hpp"
|
||||
#include "../Commodore/Vic-20/Vic20.hpp"
|
||||
@@ -33,14 +34,15 @@ namespace {
|
||||
#define Bind(m) BindD(m, m)
|
||||
switch(target->machine) {
|
||||
Bind(AmstradCPC)
|
||||
Bind(AppleII)
|
||||
BindD(Apple::II, AppleII)
|
||||
BindD(Apple::Macintosh, Macintosh)
|
||||
Bind(Atari2600)
|
||||
BindD(Coleco::Vision, ColecoVision)
|
||||
BindD(Commodore::Vic20, Vic20)
|
||||
Bind(Electron)
|
||||
BindD(Sega::MasterSystem, MasterSystem)
|
||||
Bind(MSX)
|
||||
Bind(Oric)
|
||||
BindD(Commodore::Vic20, Vic20)
|
||||
BindD(Sega::MasterSystem, MasterSystem)
|
||||
Bind(ZX8081)
|
||||
|
||||
default:
|
||||
@@ -103,6 +105,7 @@ std::string Machine::ShortNameForTargetMachine(const Analyser::Machine machine)
|
||||
case Analyser::Machine::Atari2600: return "Atari2600";
|
||||
case Analyser::Machine::ColecoVision: return "ColecoVision";
|
||||
case Analyser::Machine::Electron: return "Electron";
|
||||
case Analyser::Machine::Macintosh: return "Macintosh";
|
||||
case Analyser::Machine::MSX: return "MSX";
|
||||
case Analyser::Machine::Oric: return "Oric";
|
||||
case Analyser::Machine::Vic20: return "Vic20";
|
||||
@@ -119,6 +122,7 @@ std::string Machine::LongNameForTargetMachine(Analyser::Machine machine) {
|
||||
case Analyser::Machine::Atari2600: return "Atari 2600";
|
||||
case Analyser::Machine::ColecoVision: return "ColecoVision";
|
||||
case Analyser::Machine::Electron: return "Acorn Electron";
|
||||
case Analyser::Machine::Macintosh: return "Apple Macintosh";
|
||||
case Analyser::Machine::MSX: return "MSX";
|
||||
case Analyser::Machine::Oric: return "Oric";
|
||||
case Analyser::Machine::Vic20: return "Vic 20";
|
||||
@@ -132,7 +136,8 @@ std::map<std::string, std::vector<std::unique_ptr<Configurable::Option>>> Machin
|
||||
std::map<std::string, std::vector<std::unique_ptr<Configurable::Option>>> options;
|
||||
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::AmstradCPC), AmstradCPC::get_options()));
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::AppleII), AppleII::get_options()));
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::AppleII), Apple::II::get_options()));
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::ColecoVision), Coleco::Vision::get_options()));
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::Electron), Electron::get_options()));
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::MSX), MSX::get_options()));
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::Oric), Oric::get_options()));
|
||||
|
||||
@@ -23,6 +23,10 @@ void Memory::Fuzz(uint8_t *buffer, std::size_t size) {
|
||||
}
|
||||
}
|
||||
|
||||
void Memory::Fuzz(uint16_t *buffer, std::size_t size) {
|
||||
Fuzz(reinterpret_cast<uint8_t *>(buffer), size * sizeof(uint16_t));
|
||||
}
|
||||
|
||||
void Memory::Fuzz(std::vector<uint8_t> &buffer) {
|
||||
Fuzz(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ namespace Memory {
|
||||
/// Stores @c size random bytes from @c buffer onwards.
|
||||
void Fuzz(uint8_t *buffer, std::size_t size);
|
||||
|
||||
// Replaces all existing vector contents with random bytes.
|
||||
/// Stores @c size random 16-bit words from @c buffer onwards.
|
||||
void Fuzz(uint16_t *buffer, std::size_t size);
|
||||
|
||||
/// Replaces all existing vector contents with random bytes.
|
||||
void Fuzz(std::vector<uint8_t> &buffer);
|
||||
|
||||
}
|
||||
|
||||
15
Machines/Utility/MemoryPacker.cpp
Normal file
15
Machines/Utility/MemoryPacker.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// MemoryPacker.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 03/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "MemoryPacker.hpp"
|
||||
|
||||
void Memory::PackBigEndian16(const std::vector<uint8_t> &source, uint16_t *target) {
|
||||
for(std::size_t c = 0; c < source.size(); c += 2) {
|
||||
target[c >> 1] = uint16_t(source[c] << 8) | uint16_t(source[c+1]);
|
||||
}
|
||||
}
|
||||
24
Machines/Utility/MemoryPacker.hpp
Normal file
24
Machines/Utility/MemoryPacker.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// MemoryPacker.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 03/05/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef MemoryPacker_hpp
|
||||
#define MemoryPacker_hpp
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Memory {
|
||||
|
||||
/*!
|
||||
Copies the bytes from @c source into @c target, interpreting them
|
||||
as big-endian 16-bit data.
|
||||
*/
|
||||
void PackBigEndian16(const std::vector<uint8_t> &source, uint16_t *target);
|
||||
|
||||
}
|
||||
#endif /* MemoryPacker_hpp */
|
||||
@@ -45,6 +45,10 @@ template<typename T> class TypedDynamicMachine: public ::Machine::DynamicMachine
|
||||
return get<KeyboardMachine::Machine>();
|
||||
}
|
||||
|
||||
MouseMachine::Machine *mouse_machine() override {
|
||||
return get<MouseMachine::Machine>();
|
||||
}
|
||||
|
||||
Configurable::Device *configurable_device() override {
|
||||
return get<Configurable::Device>();
|
||||
}
|
||||
|
||||
@@ -76,7 +76,11 @@ template<bool is_zx81> class ConcreteMachine:
|
||||
clear_all_keys();
|
||||
|
||||
const bool use_zx81_rom = target.is_ZX81 || target.ZX80_uses_ZX81_ROM;
|
||||
const auto roms = rom_fetcher("ZX8081", { use_zx81_rom ? "zx81.rom" : "zx80.rom" });
|
||||
const auto roms =
|
||||
use_zx81_rom ?
|
||||
rom_fetcher({ {"ZX8081", "the ZX81 BASIC ROM", "zx81.rom", 8 * 1024, 0x4b1dd6eb} }) :
|
||||
rom_fetcher({ {"ZX8081", "the ZX80 BASIC ROM", "zx80.rom", 4 * 1024, 0x4c7fc597} });
|
||||
|
||||
if(!roms[0]) throw ROMMachine::Error::MissingROMs;
|
||||
|
||||
rom_ = std::move(*roms[0]);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:/Users/thomasharte/Projects/CLK/OSBindings/Mac/Clock SignalTests/68000ArithmeticTests.mm">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "self:Clock Signal.xcodeproj">
|
||||
</FileRef>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0930"
|
||||
LastUpgradeVersion = "1030"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,6 +26,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
disableMainThreadChecker = "YES"
|
||||
codeCoverageEnabled = "YES"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
@@ -68,7 +69,7 @@
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableASanStackUseAfterReturn = "YES"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -14,7 +14,7 @@
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="xOd-HO-29H" userLabel="Window">
|
||||
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="xOd-HO-29H" userLabel="Window">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowCollectionBehavior key="collectionBehavior" fullScreenPrimary="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
@@ -25,7 +25,7 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="450"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<openGLView hidden="YES" useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSOpenGLView">
|
||||
<openGLView hidden="YES" wantsLayer="YES" useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSOpenGLView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="450"/>
|
||||
</openGLView>
|
||||
</subviews>
|
||||
@@ -36,6 +36,11 @@
|
||||
<constraint firstItem="DEG-fq-cjd" firstAttribute="width" secondItem="gIp-Ho-8D9" secondAttribute="width" id="mYS-bH-DST"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="color" keyPath="backgroundColor">
|
||||
<color key="value" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-2" id="0bl-1N-x8E"/>
|
||||
<outlet property="initialFirstResponder" destination="DEG-fq-cjd" id="9RI-Kx-QeN"/>
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
|
||||
#import "CSStaticAnalyser.h"
|
||||
|
||||
#import "CSOpenGLView.h"
|
||||
#import "CSAudioQueue.h"
|
||||
#import "CSOpenGLView.h"
|
||||
#import "CSROMReceiverView.h"
|
||||
|
||||
#import "CSBestEffortUpdater.h"
|
||||
#import "CSJoystickManager.h"
|
||||
|
||||
#import "NSData+CRC32.h"
|
||||
|
||||
#include "KeyCodes.h"
|
||||
|
||||
@@ -16,109 +16,76 @@ class MachineDocument:
|
||||
CSOpenGLViewDelegate,
|
||||
CSOpenGLViewResponderDelegate,
|
||||
CSBestEffortUpdaterDelegate,
|
||||
CSAudioQueueDelegate
|
||||
CSAudioQueueDelegate,
|
||||
CSROMReciverViewDelegate
|
||||
{
|
||||
fileprivate let actionLock = NSLock()
|
||||
fileprivate let drawLock = NSLock()
|
||||
fileprivate let bestEffortLock = NSLock()
|
||||
// MARK: - Mutual Exclusion.
|
||||
|
||||
var machine: CSMachine!
|
||||
var name: String! {
|
||||
get {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var optionsPanelNibName: String?
|
||||
/// Ensures exclusive access between calls to self.machine.run and close().
|
||||
private let actionLock = NSLock()
|
||||
/// Ensures exclusive access between calls to machine.updateView and machine.drawView, and close().
|
||||
private let drawLock = NSLock()
|
||||
/// Ensures exclusive access to the best-effort updater.
|
||||
private let bestEffortLock = NSLock()
|
||||
|
||||
func aspectRatio() -> NSSize {
|
||||
// MARK: - Machine details.
|
||||
|
||||
/// A description of the machine this document should represent once fully set up.
|
||||
private var machineDescription: CSStaticAnalyser?
|
||||
|
||||
/// The active machine, following its successful creation.
|
||||
private var machine: CSMachine!
|
||||
|
||||
/// @returns the appropriate window content aspect ratio for this @c self.machine.
|
||||
private func aspectRatio() -> NSSize {
|
||||
return NSSize(width: 4.0, height: 3.0)
|
||||
}
|
||||
|
||||
/// The output audio queue, if any.
|
||||
private var audioQueue: CSAudioQueue!
|
||||
|
||||
/// The best-effort updater.
|
||||
private var bestEffortUpdater: CSBestEffortUpdater?
|
||||
|
||||
// MARK: - Main NIB connections.
|
||||
|
||||
/// The OpenGL view to receive this machine's display.
|
||||
@IBOutlet weak var openGLView: CSOpenGLView!
|
||||
|
||||
/// The options panel, if any.
|
||||
@IBOutlet var optionsPanel: MachinePanel!
|
||||
|
||||
/// An action to display the options panel, if there is one.
|
||||
@IBAction func showOptions(_ sender: AnyObject!) {
|
||||
optionsPanel?.setIsVisible(true)
|
||||
}
|
||||
|
||||
/// The activity panel, if one is deemed appropriate.
|
||||
@IBOutlet var activityPanel: NSPanel!
|
||||
|
||||
/// An action to display the activity panel, if there is one.
|
||||
@IBAction func showActivity(_ sender: AnyObject!) {
|
||||
activityPanel.setIsVisible(true)
|
||||
}
|
||||
|
||||
fileprivate var audioQueue: CSAudioQueue! = nil
|
||||
fileprivate var bestEffortUpdater: CSBestEffortUpdater?
|
||||
// MARK: - NSDocument Overrides and NSWindowDelegate methods.
|
||||
|
||||
/// Links this class to the MachineDocument NIB.
|
||||
override var windowNibName: NSNib.Name? {
|
||||
return NSNib.Name(rawValue: "MachineDocument")
|
||||
return "MachineDocument"
|
||||
}
|
||||
|
||||
override func windowControllerDidLoadNib(_ aController: NSWindowController) {
|
||||
super.windowControllerDidLoadNib(aController)
|
||||
aController.window?.contentAspectRatio = self.aspectRatio()
|
||||
setupMachineOutput()
|
||||
convenience init(type typeName: String) throws {
|
||||
self.init()
|
||||
self.fileType = typeName
|
||||
}
|
||||
|
||||
// Attempting to show a sheet before the window is visible (such as when the NIB is loaded) results in
|
||||
// a sheet mysteriously floating on its own. For now, use windowDidUpdate as a proxy to know that the window
|
||||
// is visible, though it's a little premature.
|
||||
func windowDidUpdate(_ notification: Notification) {
|
||||
if self.shouldShowNewMachinePanel {
|
||||
self.shouldShowNewMachinePanel = false
|
||||
Bundle.main.loadNibNamed(NSNib.Name(rawValue: "MachinePicker"), owner: self, topLevelObjects: nil)
|
||||
self.machinePicker?.establishStoredOptions()
|
||||
self.windowControllers[0].window?.beginSheet(self.machinePickerPanel!, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func setupMachineOutput() {
|
||||
if let machine = self.machine, let openGLView = self.openGLView {
|
||||
// establish the output aspect ratio and audio
|
||||
let aspectRatio = self.aspectRatio()
|
||||
openGLView.perform(glContext: {
|
||||
machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height))
|
||||
})
|
||||
|
||||
// attach an options panel if one is available
|
||||
if let optionsPanelNibName = self.optionsPanelNibName {
|
||||
Bundle.main.loadNibNamed(NSNib.Name(rawValue: optionsPanelNibName), owner: self, topLevelObjects: nil)
|
||||
self.optionsPanel.machine = machine
|
||||
self.optionsPanel?.establishStoredOptions()
|
||||
showOptions(self)
|
||||
}
|
||||
|
||||
machine.delegate = self
|
||||
self.bestEffortUpdater = CSBestEffortUpdater()
|
||||
|
||||
// callbacks from the OpenGL may come on a different thread, immediately following the .delegate set;
|
||||
// hence the full setup of the best-effort updater prior to setting self as a delegate
|
||||
openGLView.delegate = self
|
||||
openGLView.responderDelegate = self
|
||||
|
||||
setupAudioQueueClockRate()
|
||||
|
||||
// bring OpenGL view-holding window on top of the options panel and show the content
|
||||
openGLView.isHidden = false
|
||||
openGLView.window!.makeKeyAndOrderFront(self)
|
||||
openGLView.window!.makeFirstResponder(openGLView)
|
||||
|
||||
// start accepting best effort updates
|
||||
self.bestEffortUpdater!.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
func machineSpeakerDidChangeInputClock(_ machine: CSMachine) {
|
||||
setupAudioQueueClockRate()
|
||||
}
|
||||
|
||||
fileprivate func setupAudioQueueClockRate() {
|
||||
// establish and provide the audio queue, taking advice as to an appropriate sampling rate
|
||||
let maximumSamplingRate = CSAudioQueue.preferredSamplingRate()
|
||||
let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate)))
|
||||
if selectedSamplingRate > 0 {
|
||||
audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate))
|
||||
audioQueue.delegate = self
|
||||
self.machine.audioQueue = self.audioQueue
|
||||
self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize)
|
||||
override func read(from url: URL, ofType typeName: String) throws {
|
||||
if let analyser = CSStaticAnalyser(fileAt: url) {
|
||||
self.displayName = analyser.displayName
|
||||
self.configureAs(analyser)
|
||||
} else {
|
||||
throw NSError(domain: "MachineDocument", code: -1, userInfo: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,75 +115,176 @@ class MachineDocument:
|
||||
super.close()
|
||||
}
|
||||
|
||||
// MARK: configuring
|
||||
override func data(ofType typeName: String) throws -> Data {
|
||||
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
|
||||
}
|
||||
|
||||
override func windowControllerDidLoadNib(_ aController: NSWindowController) {
|
||||
super.windowControllerDidLoadNib(aController)
|
||||
aController.window?.contentAspectRatio = self.aspectRatio()
|
||||
}
|
||||
|
||||
private var missingROMs: [CSMissingROM] = []
|
||||
func configureAs(_ analysis: CSStaticAnalyser) {
|
||||
if let machine = CSMachine(analyser: analysis) {
|
||||
self.machineDescription = analysis
|
||||
|
||||
let missingROMs = NSMutableArray()
|
||||
if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) {
|
||||
self.machine = machine
|
||||
self.optionsPanelNibName = analysis.optionsPanelNibName
|
||||
setupMachineOutput()
|
||||
setupActivityDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var shouldShowNewMachinePanel = false
|
||||
override func read(from url: URL, ofType typeName: String) throws {
|
||||
if let analyser = CSStaticAnalyser(fileAt: url) {
|
||||
self.displayName = analyser.displayName
|
||||
self.configureAs(analyser)
|
||||
} else {
|
||||
throw NSError(domain: "MachineDocument", code: -1, userInfo: nil)
|
||||
// Store the selected machine and list of missing ROMs, and
|
||||
// show the missing ROMs dialogue.
|
||||
self.missingROMs = []
|
||||
for untypedMissingROM in missingROMs {
|
||||
self.missingROMs.append(untypedMissingROM as! CSMissingROM)
|
||||
}
|
||||
|
||||
requestRoms()
|
||||
}
|
||||
}
|
||||
|
||||
convenience init(type typeName: String) throws {
|
||||
self.init()
|
||||
self.fileType = typeName
|
||||
self.shouldShowNewMachinePanel = true
|
||||
enum InteractionMode {
|
||||
case notStarted, showingMachinePicker, showingROMRequester, showingMachine
|
||||
}
|
||||
private var interactionMode: InteractionMode = .notStarted
|
||||
|
||||
// MARK: the pasteboard
|
||||
func paste(_ sender: Any) {
|
||||
let pasteboard = NSPasteboard.general
|
||||
if let string = pasteboard.string(forType: .string) {
|
||||
self.machine.paste(string)
|
||||
// Attempting to show a sheet before the window is visible (such as when the NIB is loaded) results in
|
||||
// a sheet mysteriously floating on its own. For now, use windowDidUpdate as a proxy to know that the window
|
||||
// is visible, though it's a little premature.
|
||||
func windowDidUpdate(_ notification: Notification) {
|
||||
// If an interaction mode is not yet in effect, pick the proper one and display the relevant thing.
|
||||
if self.interactionMode == .notStarted {
|
||||
// If a full machine exists, just continue showing it.
|
||||
if self.machine != nil {
|
||||
self.interactionMode = .showingMachine
|
||||
setupMachineOutput()
|
||||
return
|
||||
}
|
||||
|
||||
// If a machine has been picked but is not showing, there must be ROMs missing.
|
||||
if self.machineDescription != nil {
|
||||
self.interactionMode = .showingROMRequester
|
||||
requestRoms()
|
||||
return
|
||||
}
|
||||
|
||||
// If a machine hasn't even been picked yet, show the machine picker.
|
||||
self.interactionMode = .showingMachinePicker
|
||||
Bundle.main.loadNibNamed("MachinePicker", owner: self, topLevelObjects: nil)
|
||||
self.machinePicker?.establishStoredOptions()
|
||||
self.windowControllers[0].window?.beginSheet(self.machinePickerPanel!, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: CSBestEffortUpdaterDelegate
|
||||
final func bestEffortUpdater(_ bestEffortUpdater: CSBestEffortUpdater!, runForInterval duration: TimeInterval, didSkipPreviousUpdate: Bool) {
|
||||
if actionLock.try() {
|
||||
self.machine.run(forInterval: duration)
|
||||
actionLock.unlock()
|
||||
// MARK: - Connections Between Machine and the Outside World
|
||||
|
||||
private func setupMachineOutput() {
|
||||
if let machine = self.machine, let openGLView = self.openGLView {
|
||||
// Establish the output aspect ratio and audio.
|
||||
let aspectRatio = self.aspectRatio()
|
||||
openGLView.perform(glContext: {
|
||||
machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height))
|
||||
})
|
||||
|
||||
// Attach an options panel if one is available.
|
||||
if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName {
|
||||
Bundle.main.loadNibNamed(optionsPanelNibName, owner: self, topLevelObjects: nil)
|
||||
self.optionsPanel.machine = machine
|
||||
self.optionsPanel?.establishStoredOptions()
|
||||
showOptions(self)
|
||||
}
|
||||
|
||||
machine.delegate = self
|
||||
self.bestEffortUpdater = CSBestEffortUpdater()
|
||||
|
||||
// Callbacks from the OpenGL may come on a different thread, immediately following the .delegate set;
|
||||
// hence the full setup of the best-effort updater prior to setting self as a delegate.
|
||||
openGLView.delegate = self
|
||||
openGLView.responderDelegate = self
|
||||
|
||||
// If this machine has a mouse, enable mouse capture.
|
||||
openGLView.shouldCaptureMouse = machine.hasMouse
|
||||
|
||||
setupAudioQueueClockRate()
|
||||
|
||||
// Bring OpenGL view-holding window on top of the options panel and show the content.
|
||||
openGLView.isHidden = false
|
||||
openGLView.window!.makeKeyAndOrderFront(self)
|
||||
openGLView.window!.makeFirstResponder(openGLView)
|
||||
|
||||
// Start accepting best effort updates.
|
||||
self.bestEffortUpdater!.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: CSAudioQueueDelegate
|
||||
func machineSpeakerDidChangeInputClock(_ machine: CSMachine) {
|
||||
setupAudioQueueClockRate()
|
||||
}
|
||||
|
||||
private func setupAudioQueueClockRate() {
|
||||
// establish and provide the audio queue, taking advice as to an appropriate sampling rate
|
||||
let maximumSamplingRate = CSAudioQueue.preferredSamplingRate()
|
||||
let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate)))
|
||||
if selectedSamplingRate > 0 {
|
||||
audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate))
|
||||
audioQueue.delegate = self
|
||||
self.machine.audioQueue = self.audioQueue
|
||||
self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Responds to the CSAudioQueueDelegate dry-queue warning message by requesting a machine update.
|
||||
final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) {
|
||||
bestEffortLock.lock()
|
||||
bestEffortUpdater?.update()
|
||||
bestEffortLock.unlock()
|
||||
}
|
||||
|
||||
// MARK: CSOpenGLViewDelegate
|
||||
/// Responds to the CSOpenGLViewDelegate redraw message by requesting a machine update if this is a timed
|
||||
/// request, and ordering a redraw regardless of the motivation.
|
||||
final func openGLViewRedraw(_ view: CSOpenGLView, event redrawEvent: CSOpenGLViewRedrawEvent) {
|
||||
switch redrawEvent {
|
||||
case .timer:
|
||||
bestEffortLock.lock()
|
||||
if let bestEffortUpdater = bestEffortUpdater {
|
||||
bestEffortLock.unlock()
|
||||
bestEffortUpdater.update()
|
||||
} else {
|
||||
bestEffortLock.unlock()
|
||||
}
|
||||
self.machine.updateView(forPixelSize: view.backingSize)
|
||||
fallthrough
|
||||
if redrawEvent == .timer {
|
||||
bestEffortLock.lock()
|
||||
if let bestEffortUpdater = bestEffortUpdater {
|
||||
bestEffortLock.unlock()
|
||||
bestEffortUpdater.update()
|
||||
} else {
|
||||
bestEffortLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
case .appKit:
|
||||
self.machine.drawView(forPixelSize: view.backingSize)
|
||||
if drawLock.try() {
|
||||
if redrawEvent == .timer {
|
||||
machine.updateView(forPixelSize: view.backingSize)
|
||||
}
|
||||
machine.drawView(forPixelSize: view.backingSize)
|
||||
drawLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Runtime media insertion.
|
||||
/// Responds to CSBestEffortUpdaterDelegate update message by running the machine.
|
||||
final func bestEffortUpdater(_ bestEffortUpdater: CSBestEffortUpdater!, runForInterval duration: TimeInterval, didSkipPreviousUpdate: Bool) {
|
||||
if let machine = self.machine, actionLock.try() {
|
||||
machine.run(forInterval: duration)
|
||||
actionLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Pasteboard Forwarding.
|
||||
|
||||
/// Forwards any text currently on the pasteboard into the active machine.
|
||||
func paste(_ sender: Any) {
|
||||
let pasteboard = NSPasteboard.general
|
||||
if let string = pasteboard.string(forType: .string), let machine = self.machine {
|
||||
machine.paste(string)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Runtime Media Insertion.
|
||||
|
||||
/// Delegate message to receive drag and drop files.
|
||||
final func openGLView(_ view: CSOpenGLView, didReceiveFileAt URL: URL) {
|
||||
let mediaSet = CSMediaSet(fileAt: URL)
|
||||
if let mediaSet = mediaSet {
|
||||
@@ -224,6 +292,8 @@ class MachineDocument:
|
||||
}
|
||||
}
|
||||
|
||||
/// Action for the insert menu command; displays an NSOpenPanel and then segues into the same process
|
||||
/// as if a file had been received via drag and drop.
|
||||
@IBAction final func insertMedia(_ sender: AnyObject!) {
|
||||
let openPanel = NSOpenPanel()
|
||||
openPanel.message = "Hint: you can also insert media by dragging and dropping it onto the machine's window."
|
||||
@@ -239,37 +309,40 @@ class MachineDocument:
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSDocument overrides
|
||||
override func data(ofType typeName: String) throws -> Data {
|
||||
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
|
||||
}
|
||||
// MARK: - Input Management.
|
||||
|
||||
// MARK: Input management
|
||||
/// Upon a resign key, immediately releases all ongoing input mechanisms — any currently pressed keys,
|
||||
/// and joystick and mouse inputs.
|
||||
func windowDidResignKey(_ notification: Notification) {
|
||||
if let machine = self.machine {
|
||||
machine.clearAllKeys()
|
||||
machine.joystickManager = nil
|
||||
}
|
||||
self.openGLView.releaseMouse()
|
||||
}
|
||||
|
||||
/// Upon becoming key, attaches joystick input to the machine.
|
||||
func windowDidBecomeKey(_ notification: Notification) {
|
||||
if let machine = self.machine {
|
||||
machine.joystickManager = (DocumentController.shared as! DocumentController).joystickManager
|
||||
}
|
||||
}
|
||||
|
||||
/// Forwards key down events directly to the machine.
|
||||
func keyDown(_ event: NSEvent) {
|
||||
if let machine = self.machine {
|
||||
machine.setKey(event.keyCode, characters: event.characters, isPressed: true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Forwards key up events directly to the machine.
|
||||
func keyUp(_ event: NSEvent) {
|
||||
if let machine = self.machine {
|
||||
machine.setKey(event.keyCode, characters: event.characters, isPressed: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Synthesies appropriate key up and key down events upon any change in modifiers.
|
||||
func flagsChanged(_ newModifiers: NSEvent) {
|
||||
if let machine = self.machine {
|
||||
machine.setKey(VK_Shift, characters: nil, isPressed: newModifiers.modifierFlags.contains(.shift))
|
||||
@@ -279,19 +352,199 @@ class MachineDocument:
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: New machine creation
|
||||
/// Forwards mouse movement events to the mouse.
|
||||
func mouseMoved(_ event: NSEvent) {
|
||||
if let machine = self.machine {
|
||||
machine.addMouseMotionX(event.deltaX, y: event.deltaY)
|
||||
}
|
||||
}
|
||||
|
||||
/// Forwards mouse button down events to the mouse.
|
||||
func mouseUp(_ event: NSEvent) {
|
||||
if let machine = self.machine {
|
||||
machine.setMouseButton(Int32(event.buttonNumber), isPressed: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Forwards mouse button up events to the mouse.
|
||||
func mouseDown(_ event: NSEvent) {
|
||||
if let machine = self.machine {
|
||||
machine.setMouseButton(Int32(event.buttonNumber), isPressed: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MachinePicker Outlets and Actions
|
||||
@IBOutlet var machinePicker: MachinePicker?
|
||||
@IBOutlet var machinePickerPanel: NSWindow?
|
||||
@IBAction func createMachine(_ sender: NSButton?) {
|
||||
self.configureAs(machinePicker!.selectedMachine())
|
||||
machinePicker = nil
|
||||
let selectedMachine = machinePicker!.selectedMachine()
|
||||
self.windowControllers[0].window?.endSheet(self.machinePickerPanel!)
|
||||
self.machinePicker = nil
|
||||
self.configureAs(selectedMachine)
|
||||
}
|
||||
|
||||
@IBAction func cancelCreateMachine(_ sender: NSButton?) {
|
||||
close()
|
||||
}
|
||||
|
||||
// MARK: - ROMRequester Outlets and Actions
|
||||
@IBOutlet var romRequesterPanel: NSWindow?
|
||||
@IBOutlet var romRequesterText: NSTextField?
|
||||
@IBOutlet var romReceiverErrorField: NSTextField?
|
||||
@IBOutlet var romReceiverView: CSROMReceiverView?
|
||||
private var romRequestBaseText = ""
|
||||
func requestRoms() {
|
||||
// Don't act yet if there's no window controller yet.
|
||||
if self.windowControllers.count == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Load the ROM requester dialogue.
|
||||
Bundle.main.loadNibNamed("ROMRequester", owner: self, topLevelObjects: nil)
|
||||
self.romReceiverView!.delegate = self
|
||||
self.romRequestBaseText = romRequesterText!.stringValue
|
||||
romReceiverErrorField?.alphaValue = 0.0
|
||||
|
||||
// Populate the current absentee list.
|
||||
populateMissingRomList()
|
||||
|
||||
// Show the thing.
|
||||
self.windowControllers[0].window?.beginSheet(self.romRequesterPanel!, completionHandler: nil)
|
||||
}
|
||||
|
||||
@IBAction func cancelRequestROMs(_ sender: NSButton?) {
|
||||
close()
|
||||
}
|
||||
|
||||
func populateMissingRomList() {
|
||||
// Fill in the missing details; first build a list of all the individual
|
||||
// line items.
|
||||
var requestLines: [String] = []
|
||||
for missingROM in self.missingROMs {
|
||||
if let descriptiveName = missingROM.descriptiveName {
|
||||
requestLines.append("• " + descriptiveName)
|
||||
} else {
|
||||
requestLines.append("• " + missingROM.fileName)
|
||||
}
|
||||
}
|
||||
|
||||
// Suffix everything up to the penultimate line with a semicolon;
|
||||
// the penultimate line with a semicolon and a conjunctive; the final
|
||||
// line with a full stop.
|
||||
for x in 0 ..< requestLines.count {
|
||||
if x < requestLines.count - 2 {
|
||||
requestLines[x].append(";")
|
||||
} else if x < requestLines.count - 1 {
|
||||
requestLines[x].append("; and")
|
||||
} else {
|
||||
requestLines[x].append(".")
|
||||
}
|
||||
}
|
||||
romRequesterText!.stringValue = self.romRequestBaseText + requestLines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
func romReceiverView(_ view: CSROMReceiverView, didReceiveFileAt URL: URL) {
|
||||
// Test whether the file identified matches any of the currently missing ROMs.
|
||||
// If so then remove that ROM from the missing list and update the request screen.
|
||||
// If no ROMs are still missing, start the machine.
|
||||
do {
|
||||
let fileData = try Data(contentsOf: URL)
|
||||
var didInstallRom = false
|
||||
|
||||
// Try to match by size first, CRC second. Accept that some ROMs may have
|
||||
// some additional appended data. Arbitrarily allow them to be up to 10kb
|
||||
// too large.
|
||||
var index = 0
|
||||
for missingROM in self.missingROMs {
|
||||
if fileData.count >= missingROM.size && fileData.count < missingROM.size + 10*1024 {
|
||||
// Trim to size.
|
||||
let trimmedData = fileData[0 ..< missingROM.size]
|
||||
|
||||
// Get CRC.
|
||||
if missingROM.crc32s.contains( (trimmedData as NSData).crc32 ) {
|
||||
// This ROM matches; copy it into the application library,
|
||||
// strike it from the missing ROM list and decide how to
|
||||
// proceed.
|
||||
let fileManager = FileManager.default
|
||||
let targetPath = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent("ROMImages")
|
||||
.appendingPathComponent(missingROM.machineName)
|
||||
let targetFile = targetPath
|
||||
.appendingPathComponent(missingROM.fileName)
|
||||
|
||||
do {
|
||||
try fileManager.createDirectory(atPath: targetPath.path, withIntermediateDirectories: true, attributes: nil)
|
||||
try trimmedData.write(to: targetFile)
|
||||
} catch let error {
|
||||
showRomReceiverError(error: "Couldn't write to application support directory: \(error)")
|
||||
}
|
||||
|
||||
self.missingROMs.remove(at: index)
|
||||
didInstallRom = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
index = index + 1
|
||||
}
|
||||
|
||||
if didInstallRom {
|
||||
if self.missingROMs.count == 0 {
|
||||
self.windowControllers[0].window?.endSheet(self.romRequesterPanel!)
|
||||
configureAs(self.machineDescription!)
|
||||
} else {
|
||||
populateMissingRomList()
|
||||
}
|
||||
} else {
|
||||
showRomReceiverError(error: "Didn't recognise contents of \(URL.lastPathComponent)")
|
||||
}
|
||||
} catch let error {
|
||||
showRomReceiverError(error: "Couldn't read file at \(URL.absoluteString): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Yucky ugliness follows; my experience as an iOS developer intersects poorly with
|
||||
// NSAnimationContext hence the various stateful diplications below. isShowingError
|
||||
// should be essentially a duplicate of the current alphaValue, and animationCount
|
||||
// is to resolve my inability to figure out how to cancel scheduled animations.
|
||||
private var errorText = ""
|
||||
private var isShowingError = false
|
||||
private var animationCount = 0
|
||||
private func showRomReceiverError(error: String) {
|
||||
// Set or append the new error.
|
||||
if self.errorText.count > 0 {
|
||||
self.errorText = self.errorText + "\n" + error
|
||||
} else {
|
||||
self.errorText = error
|
||||
}
|
||||
|
||||
// Apply the new complete text.
|
||||
romReceiverErrorField!.stringValue = self.errorText
|
||||
|
||||
if !isShowingError {
|
||||
// Schedule the box's appearance.
|
||||
NSAnimationContext.beginGrouping()
|
||||
NSAnimationContext.current.duration = 0.1
|
||||
romReceiverErrorField?.animator().alphaValue = 1.0
|
||||
NSAnimationContext.endGrouping()
|
||||
isShowingError = true
|
||||
}
|
||||
|
||||
// Schedule the box to disappear.
|
||||
self.animationCount = self.animationCount + 1
|
||||
let capturedAnimationCount = animationCount
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(2)) {
|
||||
if self.animationCount == capturedAnimationCount {
|
||||
NSAnimationContext.beginGrouping()
|
||||
NSAnimationContext.current.duration = 1.0
|
||||
self.romReceiverErrorField?.animator().alphaValue = 0.0
|
||||
NSAnimationContext.endGrouping()
|
||||
self.isShowingError = false
|
||||
self.errorText = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Joystick-via-the-keyboard selection
|
||||
@IBAction func useKeyboardAsKeyboard(_ sender: NSMenuItem?) {
|
||||
machine.inputMode = .keyboard
|
||||
@@ -301,6 +554,9 @@ class MachineDocument:
|
||||
machine.inputMode = .joystick
|
||||
}
|
||||
|
||||
/// Determines which of the menu items to enable and disable based on the ability of the
|
||||
/// current machine to handle keyboard and joystick input, accept new media and whether
|
||||
/// it has an associted activity window.
|
||||
override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
||||
if let menuItem = item as? NSMenuItem {
|
||||
switch item.action {
|
||||
@@ -334,7 +590,7 @@ class MachineDocument:
|
||||
return super.validateUserInterfaceItem(item)
|
||||
}
|
||||
|
||||
// Screenshot capture.
|
||||
/// Saves a screenshot of the
|
||||
@IBAction func saveScreenshot(_ sender: AnyObject!) {
|
||||
// Grab a date formatter and form a file name.
|
||||
let dateFormatter = DateFormatter()
|
||||
@@ -358,7 +614,8 @@ class MachineDocument:
|
||||
}
|
||||
|
||||
// MARK: Activity display.
|
||||
class LED {
|
||||
|
||||
private class LED {
|
||||
let levelIndicator: NSLevelIndicator
|
||||
init(levelIndicator: NSLevelIndicator) {
|
||||
self.levelIndicator = levelIndicator
|
||||
@@ -366,11 +623,12 @@ class MachineDocument:
|
||||
var isLit = false
|
||||
var isBlinking = false
|
||||
}
|
||||
fileprivate var leds: [String: LED] = [:]
|
||||
private var leds: [String: LED] = [:]
|
||||
|
||||
func setupActivityDisplay() {
|
||||
var leds = machine.leds
|
||||
if leds.count > 0 {
|
||||
Bundle.main.loadNibNamed(NSNib.Name(rawValue: "Activity"), owner: self, topLevelObjects: nil)
|
||||
Bundle.main.loadNibNamed("Activity", owner: self, topLevelObjects: nil)
|
||||
showActivity(nil)
|
||||
|
||||
// Inspect the activity panel for indicators.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// CSHighPrecisionTimer.h
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/03/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/*!
|
||||
Provides a high-precision timer; provide it with a block and an
|
||||
interval, and it will ensure the block is performed as regularly
|
||||
as the system will allow at the specified intervals.
|
||||
|
||||
The block will be executed on an arbitrary thread.
|
||||
*/
|
||||
@interface CSHighPrecisionTimer: NSObject
|
||||
|
||||
/// Initialises a new instance of the high precision timer; the timer will begin
|
||||
/// ticking immediately.
|
||||
- (instancetype)initWithTask:(dispatch_block_t)task interval:(uint64_t)interval;
|
||||
|
||||
/// Stops the timer.
|
||||
- (void)invalidate;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// CSHighPrecisionTimer.m
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/03/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#import "CSHighPrecisionTimer.h"
|
||||
|
||||
@implementation CSHighPrecisionTimer {
|
||||
dispatch_source_t _timer;
|
||||
dispatch_queue_t _queue;
|
||||
}
|
||||
|
||||
- (instancetype)initWithTask:(dispatch_block_t)task interval:(uint64_t)interval {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
_queue = dispatch_queue_create("High precision timer queue", DISPATCH_QUEUE_SERIAL);
|
||||
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _queue);
|
||||
if(_timer) {
|
||||
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval, 0);
|
||||
dispatch_source_set_event_handler(_timer, task);
|
||||
|
||||
// TODO: dispatch_activate is preferred to dispatch_resume, but arrives only
|
||||
// as of macOS 10.12. So switch if/when upping the OS requirements.
|
||||
dispatch_resume(_timer);
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)invalidate {
|
||||
dispatch_suspend(_timer);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -14,42 +14,42 @@
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cartridge.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Atari 2600 Cartridge</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Atari 2600 Cartridge</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>rom</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>chip.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ROM Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
@@ -57,110 +57,110 @@
|
||||
<string>uef</string>
|
||||
<string>uef.gz</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cassette.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Electron/BBC UEF Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>prg</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>floppy525.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Commodore Program</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>tap</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cassette.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Tape Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>g64</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>floppy525.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Commodore Disk</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>d64</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>floppy525.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Commodore 1540/1 Disk</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
@@ -171,44 +171,44 @@
|
||||
<string>adl</string>
|
||||
<string>adm</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>floppy35.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Electron/BBC Disk Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>dsk</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>floppy35.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Disk Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
@@ -216,22 +216,22 @@
|
||||
<string>o</string>
|
||||
<string>80</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cassette.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ZX80 Tape Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
@@ -240,240 +240,240 @@
|
||||
<string>81</string>
|
||||
<string>p81</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cassette.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ZX81 Tape Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>csw</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cassette.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Tape Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>tzx</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cassette.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Tape Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>cdt</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cassette.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Amstrad CPC Tape Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>hfe</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>floppy35.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>HxC Disk Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>cas</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cassette.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>MSX Tape Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>dmk</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>floppy35.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Disk Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>tsx</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cassette.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>MSX Tape Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>col</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cartridge.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ColecoVision Cartridge</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>sms</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cartridge.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Master System Cartridge</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>sg</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>cartridge.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>SG1000 Cartridge</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
@@ -483,22 +483,48 @@
|
||||
<string>do</string>
|
||||
<string>po</string>
|
||||
</array>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>floppy525.png</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Apple II Disk Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>img</string>
|
||||
<string>hfv</string>
|
||||
<string>image</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>floppy35</string>
|
||||
<key>CFBundleTypeMIMETypes</key>
|
||||
<array>
|
||||
<string>application/x-apple-diskimage</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>DiskCopy 4.2 Disk Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<integer>0</integer>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
@@ -524,7 +550,7 @@
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright 2018 Thomas Harte. All rights reserved.</string>
|
||||
<string>Copyright 2019 Thomas Harte. All rights reserved.</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -33,6 +33,14 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
|
||||
CSMachineKeyboardInputModeJoystick
|
||||
};
|
||||
|
||||
@interface CSMissingROM: NSObject
|
||||
@property (nonatomic, readonly, nonnull) NSString *machineName;
|
||||
@property (nonatomic, readonly, nonnull) NSString *fileName;
|
||||
@property (nonatomic, readonly, nullable) NSString *descriptiveName;
|
||||
@property (nonatomic, readonly) NSUInteger size;
|
||||
@property (nonatomic, readonly, nonnull) NSArray<NSNumber *> *crc32s;
|
||||
@end
|
||||
|
||||
// Deliberately low; to ensure CSMachine has been declared as an @class already.
|
||||
#import "CSAtari2600.h"
|
||||
#import "CSZX8081.h"
|
||||
@@ -45,8 +53,10 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
|
||||
Initialises an instance of CSMachine.
|
||||
|
||||
@param result The CSStaticAnalyser result that describes the machine needed.
|
||||
@param missingROMs An array that is filled with a list of ROMs that the machine requested but which
|
||||
were not found; populated only if this `init` has failed.
|
||||
*/
|
||||
- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result NS_DESIGNATED_INITIALIZER;
|
||||
- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableArray<CSMissingROM *> *)missingROMs NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (void)runForInterval:(NSTimeInterval)interval;
|
||||
|
||||
@@ -61,6 +71,9 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
|
||||
- (void)setKey:(uint16_t)key characters:(nullable NSString *)characters isPressed:(BOOL)isPressed;
|
||||
- (void)clearAllKeys;
|
||||
|
||||
- (void)setMouseButton:(int)button isPressed:(BOOL)isPressed;
|
||||
- (void)addMouseMotionX:(CGFloat)deltaX y:(CGFloat)deltaY;
|
||||
|
||||
@property (nonatomic, strong, nullable) CSAudioQueue *audioQueue;
|
||||
@property (nonatomic, readonly, nonnull) CSOpenGLView *view;
|
||||
@property (nonatomic, weak, nullable) id<CSMachineDelegate> delegate;
|
||||
@@ -81,6 +94,7 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
|
||||
// Input control.
|
||||
@property (nonatomic, readonly) BOOL hasExclusiveKeyboard;
|
||||
@property (nonatomic, readonly) BOOL hasJoystick;
|
||||
@property (nonatomic, readonly) BOOL hasMouse;
|
||||
@property (nonatomic, assign) CSMachineKeyboardInputMode inputMode;
|
||||
@property (nonatomic, nullable) CSJoystickManager *joystickManager;
|
||||
|
||||
|
||||
@@ -74,6 +74,68 @@ struct ActivityObserver: public Activity::Observer {
|
||||
__unsafe_unretained CSMachine *machine;
|
||||
};
|
||||
|
||||
@interface CSMissingROM (Mutability)
|
||||
@property (nonatomic, nonnull, copy) NSString *machineName;
|
||||
@property (nonatomic, nonnull, copy) NSString *fileName;
|
||||
@property (nonatomic, nullable, copy) NSString *descriptiveName;
|
||||
@property (nonatomic, readwrite) NSUInteger size;
|
||||
@property (nonatomic, copy) NSArray<NSNumber *> *crc32s;
|
||||
@end
|
||||
|
||||
@implementation CSMissingROM {
|
||||
NSString *_machineName;
|
||||
NSString *_fileName;
|
||||
NSString *_descriptiveName;
|
||||
NSUInteger _size;
|
||||
NSArray<NSNumber *> *_crc32s;
|
||||
}
|
||||
|
||||
- (NSString *)machineName {
|
||||
return _machineName;
|
||||
}
|
||||
|
||||
- (void)setMachineName:(NSString *)machineName {
|
||||
_machineName = [machineName copy];
|
||||
}
|
||||
|
||||
- (NSString *)fileName {
|
||||
return _fileName;
|
||||
}
|
||||
|
||||
- (void)setFileName:(NSString *)fileName {
|
||||
_fileName = [fileName copy];
|
||||
}
|
||||
|
||||
- (NSString *)descriptiveName {
|
||||
return _descriptiveName;
|
||||
}
|
||||
|
||||
- (void)setDescriptiveName:(NSString *)descriptiveName {
|
||||
_descriptiveName = [descriptiveName copy];
|
||||
}
|
||||
|
||||
- (NSUInteger)size {
|
||||
return _size;
|
||||
}
|
||||
|
||||
- (void)setSize:(NSUInteger)size {
|
||||
_size = size;
|
||||
}
|
||||
|
||||
- (NSArray<NSNumber *> *)crc32s {
|
||||
return _crc32s;
|
||||
}
|
||||
|
||||
- (void)setCrc32s:(NSArray<NSNumber *> *)crc32s {
|
||||
_crc32s = [crc32s copy];
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"%@/%@, %@ bytes, CRCs: %@", _fileName, _descriptiveName, @(_size), _crc32s];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CSMachine {
|
||||
SpeakerDelegate _speakerDelegate;
|
||||
ActivityObserver _activityObserver;
|
||||
@@ -90,14 +152,37 @@ struct ActivityObserver: public Activity::Observer {
|
||||
std::unique_ptr<Outputs::Display::OpenGL::ScanTarget> _scanTarget;
|
||||
}
|
||||
|
||||
- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result {
|
||||
- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result missingROMs:(inout NSMutableArray<CSMissingROM *> *)missingROMs {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
_analyser = result;
|
||||
|
||||
Machine::Error error;
|
||||
_machine.reset(Machine::MachineForTargets(_analyser.targets, CSROMFetcher(), error));
|
||||
if(!_machine) return nil;
|
||||
std::vector<ROMMachine::ROM> missing_roms;
|
||||
_machine.reset(Machine::MachineForTargets(_analyser.targets, CSROMFetcher(&missing_roms), error));
|
||||
if(!_machine) {
|
||||
for(const auto &missing_rom : missing_roms) {
|
||||
CSMissingROM *rom = [[CSMissingROM alloc] init];
|
||||
|
||||
// Copy/convert the primitive fields.
|
||||
rom.machineName = [NSString stringWithUTF8String:missing_rom.machine_name.c_str()];
|
||||
rom.fileName = [NSString stringWithUTF8String:missing_rom.file_name.c_str()];
|
||||
rom.descriptiveName = missing_rom.descriptive_name.empty() ? nil : [NSString stringWithUTF8String:missing_rom.descriptive_name.c_str()];
|
||||
rom.size = missing_rom.size;
|
||||
|
||||
// Convert the CRC list.
|
||||
NSMutableArray<NSNumber *> *crc32s = [[NSMutableArray alloc] init];
|
||||
for(const auto &crc : missing_rom.crc32s) {
|
||||
[crc32s addObject:@(crc)];
|
||||
}
|
||||
rom.crc32s = [crc32s copy];
|
||||
|
||||
// Add to the missing list.
|
||||
[missingROMs addObject:rom];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
_inputMode =
|
||||
(_machine->keyboard_machine() && _machine->keyboard_machine()->get_keyboard().is_exclusive())
|
||||
@@ -410,14 +495,14 @@ struct ActivityObserver: public Activity::Observer {
|
||||
}
|
||||
|
||||
- (void)clearAllKeys {
|
||||
auto keyboard_machine = _machine->keyboard_machine();
|
||||
const auto keyboard_machine = _machine->keyboard_machine();
|
||||
if(keyboard_machine) {
|
||||
@synchronized(self) {
|
||||
keyboard_machine->get_keyboard().reset_all_keys();
|
||||
}
|
||||
}
|
||||
|
||||
auto joystick_machine = _machine->joystick_machine();
|
||||
const auto joystick_machine = _machine->joystick_machine();
|
||||
if(joystick_machine) {
|
||||
@synchronized(self) {
|
||||
for(auto &joystick : joystick_machine->get_joysticks()) {
|
||||
@@ -425,6 +510,31 @@ struct ActivityObserver: public Activity::Observer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto mouse_machine = _machine->mouse_machine();
|
||||
if(mouse_machine) {
|
||||
@synchronized(self) {
|
||||
mouse_machine->get_mouse().reset_all_buttons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setMouseButton:(int)button isPressed:(BOOL)isPressed {
|
||||
auto mouse_machine = _machine->mouse_machine();
|
||||
if(mouse_machine) {
|
||||
@synchronized(self) {
|
||||
mouse_machine->get_mouse().set_button_pressed(button % mouse_machine->get_mouse().get_number_of_buttons(), isPressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addMouseMotionX:(CGFloat)deltaX y:(CGFloat)deltaY {
|
||||
auto mouse_machine = _machine->mouse_machine();
|
||||
if(mouse_machine) {
|
||||
@synchronized(self) {
|
||||
mouse_machine->get_mouse().move(int(deltaX), int(deltaY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Options
|
||||
@@ -540,6 +650,10 @@ struct ActivityObserver: public Activity::Observer {
|
||||
return !!_machine->joystick_machine();
|
||||
}
|
||||
|
||||
- (BOOL)hasMouse {
|
||||
return !!_machine->mouse_machine();
|
||||
}
|
||||
|
||||
- (BOOL)hasExclusiveKeyboard {
|
||||
return !!_machine->keyboard_machine() && _machine->keyboard_machine()->get_keyboard().is_exclusive();
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
|
||||
#include "ROMMachine.hpp"
|
||||
|
||||
ROMMachine::ROMFetcher CSROMFetcher();
|
||||
ROMMachine::ROMFetcher CSROMFetcher(std::vector<ROMMachine::ROM> *missing_roms = nullptr);
|
||||
|
||||
@@ -14,15 +14,39 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
ROMMachine::ROMFetcher CSROMFetcher() {
|
||||
return [] (const std::string &machine, const std::vector<std::string> &names) -> std::vector<std::unique_ptr<std::vector<std::uint8_t>>> {
|
||||
NSString *subDirectory = [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:machine.c_str()]];
|
||||
std::vector<std::unique_ptr<std::vector<std::uint8_t>>> results;
|
||||
for(const auto &name: names) {
|
||||
NSData *fileData = [[NSBundle mainBundle] dataForResource:[NSString stringWithUTF8String:name.c_str()] withExtension:nil subdirectory:subDirectory];
|
||||
ROMMachine::ROMFetcher CSROMFetcher(std::vector<ROMMachine::ROM> *missing_roms) {
|
||||
return [missing_roms] (const std::vector<ROMMachine::ROM> &roms) -> std::vector<std::unique_ptr<std::vector<std::uint8_t>>> {
|
||||
NSArray<NSURL *> *const supportURLs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask];
|
||||
|
||||
if(!fileData)
|
||||
std::vector<std::unique_ptr<std::vector<std::uint8_t>>> results;
|
||||
for(const auto &rom: roms) {
|
||||
NSData *fileData;
|
||||
NSString *const subdirectory = [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:rom.machine_name.c_str()]];
|
||||
|
||||
// Check for this file first within the application support directories.
|
||||
for(NSURL *supportURL in supportURLs) {
|
||||
NSURL *const fullURL = [[supportURL URLByAppendingPathComponent:subdirectory]
|
||||
URLByAppendingPathComponent:[NSString stringWithUTF8String:rom.file_name.c_str()]];
|
||||
fileData = [NSData dataWithContentsOfURL:fullURL];
|
||||
if(fileData) break;
|
||||
}
|
||||
|
||||
// Failing that, check inside the application bundle.
|
||||
if(!fileData) {
|
||||
fileData = [[NSBundle mainBundle]
|
||||
dataForResource:[NSString stringWithUTF8String:rom.file_name.c_str()]
|
||||
withExtension:nil
|
||||
subdirectory:subdirectory];
|
||||
}
|
||||
|
||||
// Store an appropriate result, accumulating a list of the missing if requested.
|
||||
if(!fileData) {
|
||||
results.emplace_back(nullptr);
|
||||
|
||||
if(missing_roms) {
|
||||
missing_roms->push_back(rom);
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::unique_ptr<std::vector<std::uint8_t>> data(new std::vector<std::uint8_t>);
|
||||
*data = fileData.stdVector8;
|
||||
|
||||
17
OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.h
Normal file
17
OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.h
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// NSData+CRC32.m
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 22/07/2019.
|
||||
// Copyright 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
@interface NSData (CRC32)
|
||||
|
||||
@property(nonnull, nonatomic, readonly) NSNumber *crc32;
|
||||
|
||||
@end
|
||||
19
OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.m
Normal file
19
OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.m
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// NSData+CRC32.m
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 22/07/2019.
|
||||
// Copyright 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSData+CRC32.h"
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
@implementation NSData (StdVector)
|
||||
|
||||
- (NSNumber *)crc32 {
|
||||
return @(crc32(crc32(0, Z_NULL, 0), self.bytes, (uInt)self.length));
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -29,6 +29,13 @@ typedef NS_ENUM(NSInteger, CSMachineCPCModel) {
|
||||
CSMachineCPCModel6128
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineMacintoshModel) {
|
||||
CSMachineMacintoshModel128k,
|
||||
CSMachineMacintoshModel512k,
|
||||
CSMachineMacintoshModel512ke,
|
||||
CSMachineMacintoshModelPlus,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineOricModel) {
|
||||
CSMachineOricModelOric1,
|
||||
CSMachineOricModelOricAtmos,
|
||||
@@ -69,6 +76,7 @@ typedef int Kilobytes;
|
||||
- (instancetype)initWithZX80MemorySize:(Kilobytes)memorySize useZX81ROM:(BOOL)useZX81ROM;
|
||||
- (instancetype)initWithZX81MemorySize:(Kilobytes)memorySize;
|
||||
- (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model diskController:(CSMachineAppleIIDiskController)diskController;
|
||||
- (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model;
|
||||
|
||||
@property(nonatomic, readonly) NSString *optionsPanelNibName;
|
||||
@property(nonatomic, readonly) NSString *displayName;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "../../../../../Analyser/Static/AmstradCPC/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/AppleII/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/Commodore/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/Macintosh/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/MSX/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/Oric/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/ZX8081/Target.hpp"
|
||||
@@ -188,7 +189,27 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
|
||||
_targets.push_back(std::move(target));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
using Target = Analyser::Static::Macintosh::Target;
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
target->machine = Analyser::Machine::Macintosh;
|
||||
|
||||
using Model = Target::Model;
|
||||
switch(model) {
|
||||
default:
|
||||
case CSMachineMacintoshModel128k: target->model = Model::Mac128k; break;
|
||||
case CSMachineMacintoshModel512k: target->model = Model::Mac512k; break;
|
||||
case CSMachineMacintoshModel512ke: target->model = Model::Mac512ke; break;
|
||||
case CSMachineMacintoshModelPlus: target->model = Model::MacPlus; break;
|
||||
}
|
||||
|
||||
_targets.push_back(std::move(target));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)optionsPanelNibName {
|
||||
@@ -196,6 +217,7 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
|
||||
case Analyser::Machine::AmstradCPC: return @"CompositeOptions";
|
||||
case Analyser::Machine::AppleII: return @"AppleIIOptions";
|
||||
case Analyser::Machine::Atari2600: return @"Atari2600Options";
|
||||
case Analyser::Machine::ColecoVision: return @"CompositeOptions";
|
||||
case Analyser::Machine::Electron: return @"QuickLoadCompositeOptions";
|
||||
case Analyser::Machine::MasterSystem: return @"CompositeOptions";
|
||||
case Analyser::Machine::MSX: return @"QuickLoadCompositeOptions";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -17,14 +17,14 @@
|
||||
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" titleVisibility="hidden" id="QvC-M9-y7g">
|
||||
<windowStyleMask key="styleMask" titled="YES" documentModal="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="600" height="205"/>
|
||||
<rect key="contentRect" x="196" y="240" width="650" height="205"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="205"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="650" height="205"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hKn-1l-OSN">
|
||||
<rect key="frame" x="499" y="13" width="87" height="32"/>
|
||||
<rect key="frame" x="549" y="13" width="87" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Choose" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="MnM-xo-4Qa">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -37,7 +37,7 @@ DQ
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JQy-Cj-AOK">
|
||||
<rect key="frame" x="418" y="13" width="82" height="32"/>
|
||||
<rect key="frame" x="468" y="13" width="82" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="sub-rB-Req">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -59,12 +59,12 @@ Gw
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<tabView translatesAutoresizingMaskIntoConstraints="NO" id="VUb-QG-x7c">
|
||||
<rect key="frame" x="13" y="51" width="574" height="140"/>
|
||||
<rect key="frame" x="13" y="51" width="624" height="140"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<tabViewItems>
|
||||
<tabViewItem label="Apple II" identifier="appleii" id="P59-QG-LOa">
|
||||
<view key="view" id="dHz-Yv-GNq">
|
||||
<rect key="frame" x="10" y="33" width="554" height="94"/>
|
||||
<rect key="frame" x="10" y="33" width="604" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="V5Z-dX-Ns4">
|
||||
@@ -130,7 +130,7 @@ Gw
|
||||
</tabViewItem>
|
||||
<tabViewItem label="Amstrad CPC" identifier="cpc" id="JmB-OF-xcM">
|
||||
<view key="view" id="5zS-Nj-Ynx">
|
||||
<rect key="frame" x="10" y="33" width="554" height="94"/>
|
||||
<rect key="frame" x="10" y="33" width="604" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="00d-sg-Krh">
|
||||
@@ -168,18 +168,18 @@ Gw
|
||||
</tabViewItem>
|
||||
<tabViewItem label="Electron" identifier="electron" id="muc-z9-Vqc">
|
||||
<view key="view" id="SRc-2D-95G">
|
||||
<rect key="frame" x="10" y="33" width="554" height="93"/>
|
||||
<rect key="frame" x="10" y="33" width="604" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JqM-IK-FMP">
|
||||
<rect key="frame" x="15" y="74" width="164" height="18"/>
|
||||
<rect key="frame" x="15" y="75" width="164" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="With Disk Filing System" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="tpW-5C-xKp">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="945-wU-JOH">
|
||||
<rect key="frame" x="15" y="54" width="228" height="18"/>
|
||||
<rect key="frame" x="15" y="55" width="228" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="With Advanced Disk Filing System" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="S0c-Jg-7Pu">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -197,9 +197,31 @@ Gw
|
||||
</constraints>
|
||||
</view>
|
||||
</tabViewItem>
|
||||
<tabViewItem label="Macintosh" identifier="mac" id="lmR-z3-xSm">
|
||||
<view key="view" id="7Yf-vi-Q0W">
|
||||
<rect key="frame" x="10" y="33" width="604" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="F6e-UC-25u">
|
||||
<rect key="frame" x="15" y="74" width="574" height="17"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="At present Clock Signal emulates only the Macintosh 512ke." id="IGV-Yp-6Af">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="F6e-UC-25u" secondAttribute="trailing" constant="17" id="42z-hS-aPq"/>
|
||||
<constraint firstItem="F6e-UC-25u" firstAttribute="leading" secondItem="7Yf-vi-Q0W" secondAttribute="leading" constant="17" id="bIg-C3-xdz"/>
|
||||
<constraint firstItem="F6e-UC-25u" firstAttribute="top" secondItem="7Yf-vi-Q0W" secondAttribute="top" constant="3" id="cxs-OP-oH5"/>
|
||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="F6e-UC-25u" secondAttribute="bottom" constant="17" id="vpF-ER-pmD"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</tabViewItem>
|
||||
<tabViewItem label="MSX" identifier="msx" id="6SR-DY-zdI">
|
||||
<view key="view" id="mWD-An-tR7">
|
||||
<rect key="frame" x="10" y="33" width="554" height="94"/>
|
||||
<rect key="frame" x="10" y="33" width="604" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8xT-Pr-8SE">
|
||||
@@ -233,6 +255,7 @@ Gw
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="LG6-mP-SeG" secondAttribute="trailing" constant="17" id="0Oc-n7-gaM"/>
|
||||
<constraint firstItem="8xT-Pr-8SE" firstAttribute="top" secondItem="LG6-mP-SeG" secondAttribute="bottom" constant="6" id="LBt-4m-GDc"/>
|
||||
<constraint firstItem="LG6-mP-SeG" firstAttribute="top" secondItem="mWD-An-tR7" secondAttribute="top" constant="3" id="bcb-ZZ-VpQ"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="8xT-Pr-8SE" secondAttribute="trailing" constant="17" id="l8P-UW-8ig"/>
|
||||
@@ -246,7 +269,7 @@ Gw
|
||||
</tabViewItem>
|
||||
<tabViewItem label="Oric" identifier="oric" id="NSx-DC-p4M">
|
||||
<view key="view" id="sOR-e0-8iZ">
|
||||
<rect key="frame" x="10" y="33" width="554" height="94"/>
|
||||
<rect key="frame" x="10" y="33" width="604" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="0ct-tf-uRH">
|
||||
@@ -311,7 +334,7 @@ Gw
|
||||
</tabViewItem>
|
||||
<tabViewItem label="Vic-20" identifier="vic20" id="cyO-PU-hSU">
|
||||
<view key="view" id="fLI-XB-QCr">
|
||||
<rect key="frame" x="10" y="33" width="554" height="94"/>
|
||||
<rect key="frame" x="10" y="33" width="604" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ueK-gq-gaF">
|
||||
@@ -388,11 +411,11 @@ Gw
|
||||
</tabViewItem>
|
||||
<tabViewItem label="ZX80" identifier="zx80" id="tMH-kF-GUz">
|
||||
<view key="view" id="8hL-Vn-Hg0">
|
||||
<rect key="frame" x="10" y="33" width="554" height="93"/>
|
||||
<rect key="frame" x="10" y="33" width="554" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="I1a-Eu-5UB">
|
||||
<rect key="frame" x="106" y="66" width="115" height="25"/>
|
||||
<rect key="frame" x="106" y="67" width="115" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Unexpanded" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="4Sa-jR-xOd" id="B8M-do-Yod">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@@ -406,7 +429,7 @@ Gw
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="NCX-4e-lSu">
|
||||
<rect key="frame" x="15" y="71" width="87" height="17"/>
|
||||
<rect key="frame" x="15" y="72" width="87" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Memory Size:" id="e6x-TE-OC5">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -414,7 +437,7 @@ Gw
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ReP-bV-Thu">
|
||||
<rect key="frame" x="15" y="47" width="114" height="18"/>
|
||||
<rect key="frame" x="15" y="48" width="114" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Use ZX81 ROM" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="VQH-nv-Pfm">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -436,11 +459,11 @@ Gw
|
||||
</tabViewItem>
|
||||
<tabViewItem label="ZX81" identifier="zx81" id="Wnn-nQ-gZ6">
|
||||
<view key="view" id="bmd-gL-gzT">
|
||||
<rect key="frame" x="10" y="33" width="554" height="93"/>
|
||||
<rect key="frame" x="10" y="33" width="554" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5aO-UX-HnX">
|
||||
<rect key="frame" x="106" y="66" width="115" height="25"/>
|
||||
<rect key="frame" x="106" y="67" width="115" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Unexpanded" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="7QC-Ij-hES" id="d3W-Gl-3Mf">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@@ -454,7 +477,7 @@ Gw
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8tU-73-XEE">
|
||||
<rect key="frame" x="15" y="71" width="87" height="17"/>
|
||||
<rect key="frame" x="15" y="72" width="87" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Memory Size:" id="z4b-oR-Yl2">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -490,7 +513,7 @@ Gw
|
||||
<constraint firstItem="VUb-QG-x7c" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" constant="20" id="zT3-Ea-QQJ"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<point key="canvasLocation" x="34" y="89"/>
|
||||
<point key="canvasLocation" x="34" y="88.5"/>
|
||||
</window>
|
||||
<customObject id="192-Eb-Rpg" customClass="MachinePicker" customModule="Clock_Signal" customModuleProvider="target">
|
||||
<connections>
|
||||
|
||||
@@ -157,6 +157,9 @@ class MachinePicker: NSObject {
|
||||
default: return CSStaticAnalyser(amstradCPCModel: .model6128)
|
||||
}
|
||||
|
||||
case "mac":
|
||||
return CSStaticAnalyser(macintoshModel: .model512ke)
|
||||
|
||||
case "msx":
|
||||
let hasDiskDrive = msxHasDiskDriveButton!.state == .on
|
||||
switch msxRegionButton!.selectedItem?.tag {
|
||||
|
||||
27
OSBindings/Mac/Clock Signal/ROMRequester/CSROMReceiverView.h
Normal file
27
OSBindings/Mac/Clock Signal/ROMRequester/CSROMReceiverView.h
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// CSROMReceiverView.h
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 22/07/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@class CSROMReceiverView;
|
||||
|
||||
@protocol CSROMReciverViewDelegate
|
||||
/*!
|
||||
Announces receipt of a file by drag and drop to the delegate.
|
||||
@param view The view making the request.
|
||||
@param URL The file URL of the received file.
|
||||
*/
|
||||
- (void)romReceiverView:(nonnull CSROMReceiverView *)view didReceiveFileAtURL:(nonnull NSURL *)URL;
|
||||
|
||||
@end
|
||||
|
||||
@interface CSROMReceiverView : NSView
|
||||
|
||||
@property(nonatomic, weak, nullable) id <CSROMReciverViewDelegate> delegate;
|
||||
|
||||
@end
|
||||
40
OSBindings/Mac/Clock Signal/ROMRequester/CSROMReceiverView.m
Normal file
40
OSBindings/Mac/Clock Signal/ROMRequester/CSROMReceiverView.m
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// CSROMReceiverView.m
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 22/07/2019.
|
||||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#import "CSROMReceiverView.h"
|
||||
|
||||
@interface CSROMReceiverView () <NSDraggingDestination>
|
||||
@end
|
||||
|
||||
|
||||
@implementation CSROMReceiverView
|
||||
|
||||
- (void)awakeFromNib {
|
||||
[super awakeFromNib];
|
||||
|
||||
// Accept file URLs by drag and drop.
|
||||
[self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]];
|
||||
}
|
||||
|
||||
#pragma mark - NSDraggingDestination
|
||||
|
||||
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
|
||||
// Just forward the URLs.
|
||||
for(NSPasteboardItem *item in [[sender draggingPasteboard] pasteboardItems]) {
|
||||
NSURL *URL = [NSURL URLWithString:[item stringForType:(__bridge NSString *)kUTTypeFileURL]];
|
||||
[self.delegate romReceiverView:self didReceiveFileAtURL:URL];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSDragOperation)draggingEntered:(id < NSDraggingInfo >)sender {
|
||||
// The emulator will take a copy of any deposited ROM files.
|
||||
return NSDragOperationCopy;
|
||||
}
|
||||
|
||||
@end
|
||||
75
OSBindings/Mac/Clock Signal/ROMRequester/ROMRequester.xib
Normal file
75
OSBindings/Mac/Clock Signal/ROMRequester/ROMRequester.xib
Normal file
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="MachineDocument" customModule="Clock_Signal" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="romReceiverErrorField" destination="Bia-0m-GxK" id="wnH-Kz-uXD"/>
|
||||
<outlet property="romReceiverView" destination="EiT-Mj-1SZ" id="Y2M-Qd-i7y"/>
|
||||
<outlet property="romRequesterPanel" destination="QvC-M9-y7g" id="saI-YH-9NP"/>
|
||||
<outlet property="romRequesterText" destination="5qG-I3-Qav" id="J9M-T8-Xyy"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" titleVisibility="hidden" id="QvC-M9-y7g">
|
||||
<windowStyleMask key="styleMask" titled="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ" customClass="CSROMReceiverView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5qG-I3-Qav">
|
||||
<rect key="frame" x="18" y="148" width="444" height="102"/>
|
||||
<textFieldCell key="cell" enabled="NO" allowsUndo="NO" id="itJ-2T-0ia">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<string key="title">Clock Signal requires you to provide images of the system ROMs for this machine. They will be stored permanently; you need do this only once.
Please drag and drop the following over this view:
|
||||
|
||||
</string>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nvl-dm-8bK">
|
||||
<rect key="frame" x="384" y="13" width="82" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="oS1-Pk-LsO">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="cancelRequestROMs:" target="-2" id="58q-yi-dlv"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Bia-0m-GxK">
|
||||
<rect key="frame" x="20" y="61" width="442" height="17"/>
|
||||
<textFieldCell key="cell" allowsUndo="NO" alignment="center" title="Multiline Label" drawsBackground="YES" id="8jl-xs-LjP">
|
||||
<font key="font" metaFont="message"/>
|
||||
<color key="textColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="tertiaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Bia-0m-GxK" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" id="9Lj-3p-7Vr"/>
|
||||
<constraint firstAttribute="trailing" secondItem="5qG-I3-Qav" secondAttribute="trailing" constant="20" id="AwT-WO-Nyf"/>
|
||||
<constraint firstAttribute="bottom" secondItem="nvl-dm-8bK" secondAttribute="bottom" constant="20" id="BU0-5T-YRH"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Bia-0m-GxK" secondAttribute="bottom" constant="61" id="ENn-7a-NGa"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Bia-0m-GxK" secondAttribute="trailing" constant="18" id="UUy-bf-87w"/>
|
||||
<constraint firstItem="5qG-I3-Qav" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" id="bRM-z1-cRf"/>
|
||||
<constraint firstAttribute="trailing" secondItem="nvl-dm-8bK" secondAttribute="trailing" constant="20" id="eQI-Nj-ane"/>
|
||||
<constraint firstItem="nvl-dm-8bK" firstAttribute="top" relation="greaterThanOrEqual" secondItem="5qG-I3-Qav" secondAttribute="bottom" constant="17" id="plB-x4-He5"/>
|
||||
<constraint firstItem="5qG-I3-Qav" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" constant="20" id="qeC-Rh-hmr"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -63,6 +63,29 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
|
||||
*/
|
||||
- (void)paste:(nonnull id)sender;
|
||||
|
||||
@optional
|
||||
|
||||
/*!
|
||||
Supplies a mouse moved event to the delegate. This functions only if
|
||||
shouldCaptureMouse is set to YES, in which case the view will ensure it captures
|
||||
the mouse and returns only relative motion
|
||||
(Cf. CGAssociateMouseAndMouseCursorPosition). It will also elide mouseDragged:
|
||||
(and rightMouseDragged:, etc) and mouseMoved: events.
|
||||
*/
|
||||
- (void)mouseMoved:(nonnull NSEvent *)event;
|
||||
|
||||
/*!
|
||||
Supplies a mouse button down event. This elides mouseDown, rightMouseDown and otherMouseDown.
|
||||
@c shouldCaptureMouse must be set to @c YES to receive these events.
|
||||
*/
|
||||
- (void)mouseDown:(nonnull NSEvent *)event;
|
||||
|
||||
/*!
|
||||
Supplies a mouse button up event. This elides mouseUp, rightMouseUp and otherMouseUp.
|
||||
@c shouldCaptureMouse must be set to @c YES to receive these events.
|
||||
*/
|
||||
- (void)mouseUp:(nonnull NSEvent *)event;
|
||||
|
||||
@end
|
||||
|
||||
/*!
|
||||
@@ -74,6 +97,8 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
|
||||
@property (atomic, weak, nullable) id <CSOpenGLViewDelegate> delegate;
|
||||
@property (nonatomic, weak, nullable) id <CSOpenGLViewResponderDelegate> responderDelegate;
|
||||
|
||||
@property (nonatomic, assign) BOOL shouldCaptureMouse;
|
||||
|
||||
/*!
|
||||
Ends the timer tracking time; should be called prior to giving up the last owning reference
|
||||
to ensure that any retain cycles implied by the timer are resolved.
|
||||
@@ -89,4 +114,9 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
|
||||
*/
|
||||
- (void)performWithGLContext:(nonnull dispatch_block_t)action;
|
||||
|
||||
/*!
|
||||
Instructs that the mouse cursor, if currently captured, should be released.
|
||||
*/
|
||||
- (void)releaseMouse;
|
||||
|
||||
@end
|
||||
|
||||
@@ -16,10 +16,15 @@
|
||||
@implementation CSOpenGLView {
|
||||
CVDisplayLinkRef _displayLink;
|
||||
CGSize _backingSize;
|
||||
|
||||
NSTrackingArea *_mouseTrackingArea;
|
||||
NSTimer *_mouseHideTimer;
|
||||
BOOL _mouseIsCaptured;
|
||||
}
|
||||
|
||||
- (void)prepareOpenGL
|
||||
{
|
||||
- (void)prepareOpenGL {
|
||||
[super prepareOpenGL];
|
||||
|
||||
// Synchronize buffer swaps with vertical refresh rate
|
||||
GLint swapInt = 1;
|
||||
[[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
|
||||
@@ -43,47 +48,43 @@
|
||||
CVDisplayLinkStart(_displayLink);
|
||||
}
|
||||
|
||||
static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext)
|
||||
{
|
||||
static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) {
|
||||
CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext;
|
||||
[view drawAtTime:now frequency:CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink)];
|
||||
return kCVReturnSuccess;
|
||||
}
|
||||
|
||||
- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency
|
||||
{
|
||||
// Draw the display now regardless of other activity.
|
||||
- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency {
|
||||
[self redrawWithEvent:CSOpenGLViewRedrawEventTimer];
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)dirtyRect {
|
||||
[self redrawWithEvent:CSOpenGLViewRedrawEventAppKit];
|
||||
}
|
||||
|
||||
- (void)redrawWithEvent:(CSOpenGLViewRedrawEvent)event {
|
||||
[self performWithGLContext:^{
|
||||
[self.delegate openGLViewRedraw:self event:CSOpenGLViewRedrawEventTimer];
|
||||
[self.delegate openGLViewRedraw:self event:event];
|
||||
CGLFlushDrawable([[self openGLContext] CGLContextObj]);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)dirtyRect
|
||||
{
|
||||
[self.delegate openGLViewRedraw:self event:CSOpenGLViewRedrawEventAppKit];
|
||||
}
|
||||
|
||||
- (void)invalidate
|
||||
{
|
||||
- (void)invalidate {
|
||||
CVDisplayLinkStop(_displayLink);
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
- (void)dealloc {
|
||||
// Release the display link
|
||||
CVDisplayLinkRelease(_displayLink);
|
||||
}
|
||||
|
||||
- (CGSize)backingSize
|
||||
{
|
||||
- (CGSize)backingSize {
|
||||
@synchronized(self) {
|
||||
return _backingSize;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reshape
|
||||
{
|
||||
- (void)reshape {
|
||||
[super reshape];
|
||||
@synchronized(self) {
|
||||
_backingSize = [self convertSizeToBacking:self.bounds.size];
|
||||
@@ -95,10 +96,8 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)awakeFromNib
|
||||
{
|
||||
NSOpenGLPixelFormatAttribute attributes[] =
|
||||
{
|
||||
- (void)awakeFromNib {
|
||||
NSOpenGLPixelFormatAttribute attributes[] = {
|
||||
NSOpenGLPFADoubleBuffer,
|
||||
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
|
||||
// NSOpenGLPFAMultisample,
|
||||
@@ -127,8 +126,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
|
||||
[self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]];
|
||||
}
|
||||
|
||||
- (void)performWithGLContext:(dispatch_block_t)action
|
||||
{
|
||||
- (void)performWithGLContext:(dispatch_block_t)action {
|
||||
CGLLockContext([[self openGLContext] CGLContextObj]);
|
||||
[self.openGLContext makeCurrentContext];
|
||||
action();
|
||||
@@ -137,47 +135,196 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
|
||||
|
||||
#pragma mark - NSResponder
|
||||
|
||||
- (BOOL)acceptsFirstResponder
|
||||
{
|
||||
- (BOOL)acceptsFirstResponder {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)keyDown:(NSEvent *)theEvent
|
||||
{
|
||||
- (void)keyDown:(NSEvent *)theEvent {
|
||||
[self.responderDelegate keyDown:theEvent];
|
||||
}
|
||||
|
||||
- (void)keyUp:(NSEvent *)theEvent
|
||||
{
|
||||
- (void)keyUp:(NSEvent *)theEvent {
|
||||
[self.responderDelegate keyUp:theEvent];
|
||||
}
|
||||
|
||||
- (void)flagsChanged:(NSEvent *)theEvent
|
||||
{
|
||||
- (void)flagsChanged:(NSEvent *)theEvent {
|
||||
[self.responderDelegate flagsChanged:theEvent];
|
||||
|
||||
// Release the mouse upon a control + command.
|
||||
if(_mouseIsCaptured &&
|
||||
theEvent.modifierFlags & NSEventModifierFlagControl &&
|
||||
theEvent.modifierFlags & NSEventModifierFlagCommand) {
|
||||
[self releaseMouse];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)paste:(id)sender
|
||||
{
|
||||
- (void)paste:(id)sender {
|
||||
[self.responderDelegate paste:sender];
|
||||
}
|
||||
|
||||
#pragma mark - NSDraggingDestination
|
||||
|
||||
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
|
||||
{
|
||||
for(NSPasteboardItem *item in [[sender draggingPasteboard] pasteboardItems])
|
||||
{
|
||||
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
|
||||
for(NSPasteboardItem *item in [[sender draggingPasteboard] pasteboardItems]) {
|
||||
NSURL *URL = [NSURL URLWithString:[item stringForType:(__bridge NSString *)kUTTypeFileURL]];
|
||||
[self.delegate openGLView:self didReceiveFileAtURL:URL];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSDragOperation)draggingEntered:(id < NSDraggingInfo >)sender
|
||||
{
|
||||
// we'll drag and drop, yeah?
|
||||
- (NSDragOperation)draggingEntered:(id < NSDraggingInfo >)sender {
|
||||
return NSDragOperationLink;
|
||||
}
|
||||
|
||||
#pragma mark - Mouse hiding
|
||||
|
||||
- (void)setShouldCaptureMouse:(BOOL)shouldCaptureMouse {
|
||||
_shouldCaptureMouse = shouldCaptureMouse;
|
||||
}
|
||||
|
||||
- (void)updateTrackingAreas {
|
||||
[super updateTrackingAreas];
|
||||
|
||||
if(_mouseTrackingArea) {
|
||||
[self removeTrackingArea:_mouseTrackingArea];
|
||||
}
|
||||
_mouseTrackingArea =
|
||||
[[NSTrackingArea alloc]
|
||||
initWithRect:self.bounds
|
||||
options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveWhenFirstResponder
|
||||
owner:self
|
||||
userInfo:nil];
|
||||
[self addTrackingArea:_mouseTrackingArea];
|
||||
}
|
||||
|
||||
- (void)scheduleMouseHide {
|
||||
if(!self.shouldCaptureMouse) {
|
||||
[_mouseHideTimer invalidate];
|
||||
|
||||
_mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
|
||||
[NSCursor setHiddenUntilMouseMoves:YES];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mouseEntered:(NSEvent *)event {
|
||||
[super mouseEntered:event];
|
||||
[self scheduleMouseHide];
|
||||
}
|
||||
|
||||
- (void)mouseExited:(NSEvent *)event {
|
||||
[super mouseExited:event];
|
||||
[_mouseHideTimer invalidate];
|
||||
_mouseHideTimer = nil;
|
||||
}
|
||||
|
||||
- (void)releaseMouse {
|
||||
if(_mouseIsCaptured) {
|
||||
_mouseIsCaptured = NO;
|
||||
CGAssociateMouseAndMouseCursorPosition(true);
|
||||
[NSCursor unhide];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Mouse motion
|
||||
|
||||
- (void)applyMouseMotion:(NSEvent *)event {
|
||||
if(!self.shouldCaptureMouse) {
|
||||
// Mouse capture is off, so don't play games with the cursor, just schedule it to
|
||||
// hide in the near future.
|
||||
[self scheduleMouseHide];
|
||||
} else {
|
||||
if(_mouseIsCaptured) {
|
||||
// Mouse capture is on, so move the cursor back to the middle of the window, and
|
||||
// forward the deltas to the listener.
|
||||
//
|
||||
// TODO: should I really need to invert the y coordinate myself? It suggests I
|
||||
// might have an error in mapping here.
|
||||
const NSPoint windowCentre = [self convertPoint:CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5) toView:nil];
|
||||
const NSPoint screenCentre = [self.window convertPointToScreen:windowCentre];
|
||||
const CGRect screenFrame = self.window.screen.frame;
|
||||
CGWarpMouseCursorPosition(NSMakePoint(
|
||||
screenFrame.origin.x + screenCentre.x,
|
||||
screenFrame.origin.y + screenFrame.size.height - screenCentre.y
|
||||
));
|
||||
|
||||
[self.responderDelegate mouseMoved:event];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mouseDragged:(NSEvent *)event {
|
||||
[self applyMouseMotion:event];
|
||||
[super mouseDragged:event];
|
||||
}
|
||||
|
||||
- (void)rightMouseDragged:(NSEvent *)event {
|
||||
[self applyMouseMotion:event];
|
||||
[super rightMouseDragged:event];
|
||||
}
|
||||
|
||||
- (void)otherMouseDragged:(NSEvent *)event {
|
||||
[self applyMouseMotion:event];
|
||||
[super otherMouseDragged:event];
|
||||
}
|
||||
|
||||
- (void)mouseMoved:(NSEvent *)event {
|
||||
[self applyMouseMotion:event];
|
||||
[super mouseMoved:event];
|
||||
}
|
||||
|
||||
#pragma mark - Mouse buttons
|
||||
|
||||
- (void)applyButtonDown:(NSEvent *)event {
|
||||
if(self.shouldCaptureMouse) {
|
||||
if(!_mouseIsCaptured) {
|
||||
_mouseIsCaptured = YES;
|
||||
[NSCursor hide];
|
||||
CGAssociateMouseAndMouseCursorPosition(false);
|
||||
|
||||
// Don't report the first click to the delegate; treat that as merely
|
||||
// an invitation to capture the cursor.
|
||||
return;
|
||||
}
|
||||
|
||||
[self.responderDelegate mouseDown:event];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applyButtonUp:(NSEvent *)event {
|
||||
if(self.shouldCaptureMouse) {
|
||||
[self.responderDelegate mouseUp:event];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mouseDown:(NSEvent *)event {
|
||||
[self applyButtonDown:event];
|
||||
[super mouseDown:event];
|
||||
}
|
||||
|
||||
- (void)rightMouseDown:(NSEvent *)event {
|
||||
[self applyButtonDown:event];
|
||||
[super rightMouseDown:event];
|
||||
}
|
||||
|
||||
- (void)otherMouseDown:(NSEvent *)event {
|
||||
[self applyButtonDown:event];
|
||||
[super otherMouseDown:event];
|
||||
}
|
||||
|
||||
- (void)mouseUp:(NSEvent *)event {
|
||||
[self applyButtonUp:event];
|
||||
[super mouseUp:event];
|
||||
}
|
||||
|
||||
- (void)rightMouseUp:(NSEvent *)event {
|
||||
[self applyButtonUp:event];
|
||||
[super rightMouseUp:event];
|
||||
}
|
||||
|
||||
- (void)otherMouseUp:(NSEvent *)event {
|
||||
[self applyButtonUp:event];
|
||||
[super otherMouseUp:event];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -11,118 +11,244 @@ import Foundation
|
||||
|
||||
class MOS6522Tests: XCTestCase {
|
||||
|
||||
fileprivate func with6522(_ action: (MOS6522Bridge) -> ()) {
|
||||
let bridge = MOS6522Bridge()
|
||||
action(bridge)
|
||||
private var m6522: MOS6522Bridge!
|
||||
|
||||
override func setUp() {
|
||||
m6522 = MOS6522Bridge()
|
||||
}
|
||||
|
||||
// MARK: Timer tests
|
||||
|
||||
func testTimerCount() {
|
||||
with6522 {
|
||||
// set timer 1 to a value of $000a
|
||||
$0.setValue(10, forRegister: 4)
|
||||
$0.setValue(0, forRegister: 5)
|
||||
// set timer 1 to a value of m652200a
|
||||
m6522.setValue(10, forRegister: 4)
|
||||
m6522.setValue(0, forRegister: 5)
|
||||
|
||||
// complete the setting cycle
|
||||
$0.run(forHalfCycles: 2)
|
||||
// complete the setting cycle
|
||||
m6522.run(forHalfCycles: 2)
|
||||
|
||||
// run for 5 cycles
|
||||
$0.run(forHalfCycles: 10)
|
||||
// run for 5 cycles
|
||||
m6522.run(forHalfCycles: 10)
|
||||
|
||||
// check that the timer has gone down by 5
|
||||
XCTAssert($0.value(forRegister: 4) == 5, "Low order byte should be 5; was \($0.value(forRegister: 4))")
|
||||
XCTAssert($0.value(forRegister: 5) == 0, "High order byte should be 0; was \($0.value(forRegister: 5))")
|
||||
}
|
||||
// check that the timer has gone down by 5
|
||||
XCTAssert(m6522.value(forRegister: 4) == 5, "Low order byte should be 5; was \(m6522.value(forRegister: 4))")
|
||||
XCTAssert(m6522.value(forRegister: 5) == 0, "High order byte should be 0; was \(m6522.value(forRegister: 5))")
|
||||
}
|
||||
|
||||
func testTimerLatches() {
|
||||
with6522 {
|
||||
// set timer 2 to $1020
|
||||
$0.setValue(0x10, forRegister: 8)
|
||||
$0.setValue(0x20, forRegister: 9)
|
||||
// set timer 2 to $1020
|
||||
m6522.setValue(0x10, forRegister: 8)
|
||||
m6522.setValue(0x20, forRegister: 9)
|
||||
|
||||
// change the low-byte latch
|
||||
$0.setValue(0x40, forRegister: 8)
|
||||
// change the low-byte latch
|
||||
m6522.setValue(0x40, forRegister: 8)
|
||||
|
||||
// complete the cycle
|
||||
$0.run(forHalfCycles: 2)
|
||||
// complete the cycle
|
||||
m6522.run(forHalfCycles: 2)
|
||||
|
||||
// chek that the new latched value hasn't been copied
|
||||
XCTAssert($0.value(forRegister: 8) == 0x10, "Low order byte should be 0x10; was \($0.value(forRegister: 8))")
|
||||
XCTAssert($0.value(forRegister: 9) == 0x20, "High order byte should be 0x20; was \($0.value(forRegister: 9))")
|
||||
// chek that the new latched value hasn't been copied
|
||||
XCTAssert(m6522.value(forRegister: 8) == 0x10, "Low order byte should be 0x10; was \(m6522.value(forRegister: 8))")
|
||||
XCTAssert(m6522.value(forRegister: 9) == 0x20, "High order byte should be 0x20; was \(m6522.value(forRegister: 9))")
|
||||
|
||||
// write the low-byte latch
|
||||
$0.setValue(0x50, forRegister: 9)
|
||||
// write the low-byte latch
|
||||
m6522.setValue(0x50, forRegister: 9)
|
||||
|
||||
// complete the cycle
|
||||
$0.run(forHalfCycles: 2)
|
||||
// complete the cycle
|
||||
m6522.run(forHalfCycles: 2)
|
||||
|
||||
// chek that the latched value has been copied
|
||||
XCTAssert($0.value(forRegister: 8) == 0x40, "Low order byte should be 0x50; was \($0.value(forRegister: 8))")
|
||||
XCTAssert($0.value(forRegister: 9) == 0x50, "High order byte should be 0x40; was \($0.value(forRegister: 9))")
|
||||
}
|
||||
// chek that the latched value has been copied
|
||||
XCTAssert(m6522.value(forRegister: 8) == 0x40, "Low order byte should be 0x50; was \(m6522.value(forRegister: 8))")
|
||||
XCTAssert(m6522.value(forRegister: 9) == 0x50, "High order byte should be 0x40; was \(m6522.value(forRegister: 9))")
|
||||
}
|
||||
|
||||
func testTimerReload() {
|
||||
with6522 {
|
||||
// set timer 1 to a value of $0010, enable repeating mode
|
||||
$0.setValue(16, forRegister: 4)
|
||||
$0.setValue(0, forRegister: 5)
|
||||
$0.setValue(0x40, forRegister: 11)
|
||||
$0.setValue(0x40 | 0x80, forRegister: 14)
|
||||
// set timer 1 to a value of m6522010, enable repeating mode
|
||||
m6522.setValue(16, forRegister: 4)
|
||||
m6522.setValue(0, forRegister: 5)
|
||||
m6522.setValue(0x40, forRegister: 11)
|
||||
m6522.setValue(0x40 | 0x80, forRegister: 14)
|
||||
|
||||
// complete the cycle to set initial values
|
||||
$0.run(forHalfCycles: 2)
|
||||
// complete the cycle to set initial values
|
||||
m6522.run(forHalfCycles: 2)
|
||||
|
||||
// run for 16 cycles
|
||||
$0.run(forHalfCycles: 32)
|
||||
// run for 16 cycles
|
||||
m6522.run(forHalfCycles: 32)
|
||||
|
||||
// check that the timer has gone down to 0 but not yet triggered an interrupt
|
||||
XCTAssert($0.value(forRegister: 4) == 0, "Low order byte should be 0; was \($0.value(forRegister: 4))")
|
||||
XCTAssert($0.value(forRegister: 5) == 0, "High order byte should be 0; was \($0.value(forRegister: 5))")
|
||||
XCTAssert(!$0.irqLine, "IRQ should not yet be active")
|
||||
// check that the timer has gone down to 0 but not yet triggered an interrupt
|
||||
XCTAssert(m6522.value(forRegister: 4) == 0, "Low order byte should be 0; was \(m6522.value(forRegister: 4))")
|
||||
XCTAssert(m6522.value(forRegister: 5) == 0, "High order byte should be 0; was \(m6522.value(forRegister: 5))")
|
||||
XCTAssert(!m6522.irqLine, "IRQ should not yet be active")
|
||||
|
||||
// check that two half-cycles later the timer is $ffff but IRQ still hasn't triggered
|
||||
$0.run(forHalfCycles: 2)
|
||||
XCTAssert($0.value(forRegister: 4) == 0xff, "Low order byte should be 0xff; was \($0.value(forRegister: 4))")
|
||||
XCTAssert($0.value(forRegister: 5) == 0xff, "High order byte should be 0xff; was \($0.value(forRegister: 5))")
|
||||
XCTAssert(!$0.irqLine, "IRQ should not yet be active")
|
||||
// check that two half-cycles later the timer is $ffff but IRQ still hasn't triggered
|
||||
m6522.run(forHalfCycles: 2)
|
||||
XCTAssert(m6522.value(forRegister: 4) == 0xff, "Low order byte should be 0xff; was \(m6522.value(forRegister: 4))")
|
||||
XCTAssert(m6522.value(forRegister: 5) == 0xff, "High order byte should be 0xff; was \(m6522.value(forRegister: 5))")
|
||||
XCTAssert(!m6522.irqLine, "IRQ should not yet be active")
|
||||
|
||||
// check that one half-cycle later the timer is still $ffff and IRQ has triggered...
|
||||
$0.run(forHalfCycles: 1)
|
||||
XCTAssert($0.irqLine, "IRQ should be active")
|
||||
XCTAssert($0.value(forRegister: 4) == 0xff, "Low order byte should be 0xff; was \($0.value(forRegister: 4))")
|
||||
XCTAssert($0.value(forRegister: 5) == 0xff, "High order byte should be 0xff; was \($0.value(forRegister: 5))")
|
||||
// check that one half-cycle later the timer is still $ffff and IRQ has triggered...
|
||||
m6522.run(forHalfCycles: 1)
|
||||
XCTAssert(m6522.irqLine, "IRQ should be active")
|
||||
XCTAssert(m6522.value(forRegister: 4) == 0xff, "Low order byte should be 0xff; was \(m6522.value(forRegister: 4))")
|
||||
XCTAssert(m6522.value(forRegister: 5) == 0xff, "High order byte should be 0xff; was \(m6522.value(forRegister: 5))")
|
||||
|
||||
// ... but that reading the timer cleared the interrupt
|
||||
XCTAssert(!$0.irqLine, "IRQ should be active")
|
||||
// ... but that reading the timer cleared the interrupt
|
||||
XCTAssert(!m6522.irqLine, "IRQ should be active")
|
||||
|
||||
// check that one half-cycles later the timer has reloaded
|
||||
$0.run(forHalfCycles: 1)
|
||||
XCTAssert($0.value(forRegister: 4) == 0x10, "Low order byte should be 0x10; was \($0.value(forRegister: 4))")
|
||||
XCTAssert($0.value(forRegister: 5) == 0x00, "High order byte should be 0x00; was \($0.value(forRegister: 5))")
|
||||
}
|
||||
// check that one half-cycles later the timer has reloaded
|
||||
m6522.run(forHalfCycles: 1)
|
||||
XCTAssert(m6522.value(forRegister: 4) == 0x10, "Low order byte should be 0x10; was \(m6522.value(forRegister: 4))")
|
||||
XCTAssert(m6522.value(forRegister: 5) == 0x00, "High order byte should be 0x00; was \(m6522.value(forRegister: 5))")
|
||||
}
|
||||
|
||||
|
||||
// MARK: Data direction tests
|
||||
func testDataDirection() {
|
||||
with6522 {
|
||||
// set low four bits of register B as output, the top four as input
|
||||
$0.setValue(0xf0, forRegister: 2)
|
||||
// set low four bits of register B as output, the top four as input
|
||||
m6522.setValue(0xf0, forRegister: 2)
|
||||
|
||||
// ask to output 0x8c
|
||||
$0.setValue(0x8c, forRegister: 0)
|
||||
// ask to output 0x8c
|
||||
m6522.setValue(0x8c, forRegister: 0)
|
||||
|
||||
// complete the cycle
|
||||
$0.run(forHalfCycles: 2)
|
||||
// complete the cycle
|
||||
m6522.run(forHalfCycles: 2)
|
||||
|
||||
// set current input as 0xda
|
||||
$0.portBInput = 0xda
|
||||
// set current input as 0xda
|
||||
m6522.portBInput = 0xda
|
||||
|
||||
// test that the result of reading register B is therefore 0x8a
|
||||
XCTAssert($0.value(forRegister: 0) == 0x8a, "Data direction register should mix input and output; got \($0.value(forRegister: 0))")
|
||||
// test that the result of reading register B is therefore 0x8a
|
||||
XCTAssert(m6522.value(forRegister: 0) == 0x8a, "Data direction register should mix input and output; got \(m6522.value(forRegister: 0))")
|
||||
}
|
||||
|
||||
func testShiftDisabled() {
|
||||
/*
|
||||
Mode 0 disables the Shift Register. In this mode the microprocessor can
|
||||
write or read the SR and the SR will shift on each CB1 positive edge
|
||||
shifting in the value on CB2. In this mode the SR Interrupt Flag is
|
||||
disabled (held to a logic 0).
|
||||
*/
|
||||
}
|
||||
|
||||
func testShiftInUnderT2() {
|
||||
/*
|
||||
In mode 1, the shifting rate is controlled by the low order 8 bits of T2
|
||||
(Figure 22). Shift pulses are generated on the CB1 pin to control shifting
|
||||
in external devices. The time between transitions of this output clock is a
|
||||
function of the system clock period and the contents of the low order T2
|
||||
latch (N).
|
||||
|
||||
The shifting operation is triggered by the read or write of the SR if the
|
||||
SR flag is set in the IFR. Otherwise the first shift will occur at the next
|
||||
time-out of T2 after a read or write of the SR. Data is shifted first into
|
||||
the low order bit of SR and is then shifted into the next higher order bit
|
||||
of the shift register on the negative-going edge of each clock pulse. The
|
||||
input data should change before the positive-going edge of the CB1 clock
|
||||
pulse. This data is shifted into shift register during the 02 clock cycle
|
||||
following the positive-going edge of the CB1 clock pulse. After 8 CB1 clock
|
||||
pulses, the shift register interrupt flag will set and IRQ will go low.
|
||||
*/
|
||||
}
|
||||
|
||||
func testShiftInUnderPhase2() {
|
||||
/*
|
||||
In mode 2, the shift rate is a direct function of the system clock
|
||||
frequency (Figure 23). CB1 becomes an output which generates shift pulses
|
||||
for controlling external devices. Timer 2 operates as an independent
|
||||
interval timer and has no effect on SR. The shifting operation is triggered
|
||||
by reading or writing the Shift Register. Data is shifted, first into bit 0
|
||||
and is then shifted into the next higher order bit of the shift register on
|
||||
the trailing edge of each 02 clock pulse. After 8 clock pulses, the shift
|
||||
register interrupt flag will be set, and the output clock pulses on CB1
|
||||
will stop.
|
||||
*/
|
||||
}
|
||||
|
||||
func testShiftInUnderCB1() {
|
||||
/*
|
||||
In mode 3, external pin CB1 becomes an input (Figure 24). This allows an
|
||||
external device to load the shift register at its own pace. The shift
|
||||
register counter will interrupt the processor each time 8 bits have been
|
||||
shifted in. However the shift register counter does not stop the shifting
|
||||
operation; it acts simply as a pulse counter. Reading or writing the Shift
|
||||
Register resets the Interrupt Flag and initializes the SR counter to count
|
||||
another 8 pulses.
|
||||
|
||||
Note that the data is shifted during the first system clock cycle
|
||||
following the positive-going edge of the CB1 shift pulse. For this reason,
|
||||
data must be held stable during the first full cycle following CB1 going
|
||||
high.
|
||||
*/
|
||||
}
|
||||
|
||||
func testShiftOutUnderT2FreeRunning() {
|
||||
/*
|
||||
Mode 4 is very similar to mode 5 in which the shifting rate is set by T2.
|
||||
However, in mode 4 the SR Counter does not stop the shifting operation
|
||||
(Figure 25). Since the Shift Register bit 7 (SR7) is recirculated back into
|
||||
bit 0, the 8 bits loaded into the Shift Register will be clocked onto CB2
|
||||
repetitively. In this mode the Shift Register Counter is disabled.
|
||||
*/
|
||||
}
|
||||
|
||||
func testShiftOutUnderT2() {
|
||||
/*
|
||||
In mode 5, the shift rate is controlled by T2 (as in mode 4). The shifting
|
||||
operation is triggered by the read or write of the SR if the SR flag is set
|
||||
in the IFR (Figure 26). Otherwise the first shift will occur at the next
|
||||
time-out of T2 after a read or write of the SR. However, with each read or
|
||||
write of the shift register the SR Counter is reset and 8 bits are shifted
|
||||
onto CB2. At the same time, 8 shift pulses are generated on CB1 to control
|
||||
shifting in external devices. After the 8 shift pulses, the shifting is
|
||||
disabled, the SR Interrupt Flag is set and CB2 remains at the last data
|
||||
level.
|
||||
*/
|
||||
}
|
||||
|
||||
func testShiftOutUnderPhase2() {
|
||||
/*
|
||||
In mode 6, the shift rate is controlled by the 02 system clock (Figure 27).
|
||||
|
||||
(... and I'm assuming the same behaviour as shift out under control of T2
|
||||
otherwise, based on original context)
|
||||
*/
|
||||
// Set the shift register to a non-zero something.
|
||||
m6522.setValue(0xaa, forRegister: 10)
|
||||
|
||||
// Set shift register mode 6.
|
||||
m6522.setValue(6 << 2, forRegister: 11)
|
||||
|
||||
// Make sure the shift register's interrupt bit is set.
|
||||
m6522.run(forHalfCycles: 16)
|
||||
XCTAssertEqual(m6522.value(forRegister: 13) & 0x04, 0x04)
|
||||
|
||||
// Test that output is now inhibited: CB2 should remain unchanged.
|
||||
let initialOutput = m6522.value(forControlLine: .two, port: .B)
|
||||
for _ in 1...8 {
|
||||
m6522.run(forHalfCycles: 2)
|
||||
XCTAssertEqual(m6522.value(forControlLine: .two, port: .B), initialOutput)
|
||||
}
|
||||
|
||||
// Set a new value to the shift register.
|
||||
m6522.setValue(0x16, forRegister: 10)
|
||||
|
||||
// Test that the new value is shifted out.
|
||||
var output = 0
|
||||
for _ in 1..<8 {
|
||||
m6522.run(forHalfCycles: 2)
|
||||
output = (output << 1) | (m6522.value(forControlLine: .two, port: .B) ? 1 : 0)
|
||||
}
|
||||
XCTAssertEqual(output, 0x16)
|
||||
}
|
||||
|
||||
func testShiftOutUnderCB1() {
|
||||
/*
|
||||
In mode 7, shifting is controlled by pulses applied to the CB1 pin by an
|
||||
external device (Figure 28). The SR counter sets the SR Interrupt Flag each
|
||||
time it counts 8 pulses but it does not disable the shifting function. Each
|
||||
time the microprocessor, writes or reads the shift register, the SR
|
||||
Interrupt Flag is reset and the SR counter is initialized to begin counting
|
||||
the next 8 shift pulses on pin CB1. After 8 shift pulses, the Interrupt
|
||||
Flag is set. The microprocessor can then load the shift register with the
|
||||
next byte of data.
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
BIN
OSBindings/Mac/Clock SignalTests/68000 Coverage/OPCLOGR2.BIN
Executable file
BIN
OSBindings/Mac/Clock SignalTests/68000 Coverage/OPCLOGR2.BIN
Executable file
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
This file provides a record of which opcodes are valid and which are invalid on a
|
||||
real 68000. It was generated by AtariZoll and made available via
|
||||
http://www.atari-forum.com/viewtopic.php?f=68&t=26820 .
|
||||
|
||||
No licence was specified.
|
||||
1580
OSBindings/Mac/Clock SignalTests/68000ArithmeticTests.mm
Normal file
1580
OSBindings/Mac/Clock SignalTests/68000ArithmeticTests.mm
Normal file
File diff suppressed because it is too large
Load Diff
291
OSBindings/Mac/Clock SignalTests/68000BCDTests.mm
Normal file
291
OSBindings/Mac/Clock SignalTests/68000BCDTests.mm
Normal file
@@ -0,0 +1,291 @@
|
||||
//
|
||||
// 68000BCDTests.m
|
||||
// Clock SignalTests
|
||||
//
|
||||
// Created by Thomas Harte on 29/06/2019.
|
||||
//
|
||||
// Largely ported from the tests of the Portable 68k Emulator.
|
||||
//
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include "TestRunner68000.hpp"
|
||||
|
||||
@interface M68000BCDTests : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation M68000BCDTests {
|
||||
std::unique_ptr<RAM68000> _machine;
|
||||
}
|
||||
|
||||
- (void)setUp {
|
||||
_machine.reset(new RAM68000());
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
_machine.reset();
|
||||
}
|
||||
|
||||
// MARK: ABCD
|
||||
|
||||
- (void)testABCD {
|
||||
_machine->set_program({
|
||||
0xc302, // ABCD D2, D1
|
||||
});
|
||||
auto state = _machine->get_processor_state();
|
||||
state.data[1] = 0x1234567a;
|
||||
state.data[2] = 0xf745ff78;
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssert(state.status & Flag::Carry);
|
||||
XCTAssertEqual(state.data[1], 0x12345658);
|
||||
XCTAssertEqual(state.data[2], 0xf745ff78);
|
||||
}
|
||||
|
||||
- (void)testABCDZero {
|
||||
_machine->set_program({
|
||||
0xc302, // ABCD D2, D1
|
||||
});
|
||||
auto state = _machine->get_processor_state();
|
||||
state.data[1] = 0x12345600;
|
||||
state.data[2] = 0x12345600;
|
||||
state.status = Flag::Zero;
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssert(state.status & Flag::Zero);
|
||||
XCTAssertEqual(state.data[1], 0x12345600);
|
||||
XCTAssertEqual(state.data[2], 0x12345600);
|
||||
}
|
||||
|
||||
- (void)testABCDNegative {
|
||||
_machine->set_program({
|
||||
0xc302, // ABCD D2, D1
|
||||
});
|
||||
auto state = _machine->get_processor_state();
|
||||
state.data[1] = 0x12345645;
|
||||
state.data[2] = 0x12345654;
|
||||
state.status = Flag::Zero;
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssert(state.status & Flag::Negative);
|
||||
XCTAssertEqual(state.data[1], 0x12345699);
|
||||
XCTAssertEqual(state.data[2], 0x12345654);
|
||||
}
|
||||
|
||||
- (void)testABCDWithX {
|
||||
_machine->set_program({
|
||||
0xc302, // ABCD D2, D1
|
||||
});
|
||||
auto state = _machine->get_processor_state();
|
||||
state.data[1] = 0x12345645;
|
||||
state.data[2] = 0x12345654;
|
||||
state.status = Flag::Extend;
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssert(state.status & Flag::Carry);
|
||||
XCTAssertEqual(state.data[1], 0x12345600);
|
||||
XCTAssertEqual(state.data[2], 0x12345654);
|
||||
}
|
||||
|
||||
- (void)testABCDOverflow {
|
||||
_machine->set_program({
|
||||
0xc302, // ABCD D2, D1
|
||||
});
|
||||
auto state = _machine->get_processor_state();
|
||||
state.data[1] = 0x1234563e;
|
||||
state.data[2] = 0x1234563e;
|
||||
state.status = Flag::Extend;
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssert(state.status & Flag::Overflow);
|
||||
XCTAssertEqual(state.data[1], 0x12345683);
|
||||
XCTAssertEqual(state.data[2], 0x1234563e);
|
||||
}
|
||||
|
||||
- (void)testABCDPredecDifferent {
|
||||
_machine->set_program({
|
||||
0xc30a, // ABCD -(A2), -(A1)
|
||||
});
|
||||
*_machine->ram_at(0x3000) = 0xa200;
|
||||
*_machine->ram_at(0x4000) = 0x1900;
|
||||
|
||||
auto state = _machine->get_processor_state();
|
||||
state.address[1] = 0x3001;
|
||||
state.address[2] = 0x4001;
|
||||
state.status = Flag::Extend;
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssert(state.status & Flag::Carry);
|
||||
XCTAssert(state.status & Flag::Extend);
|
||||
XCTAssertEqual(state.address[1], 0x3000);
|
||||
XCTAssertEqual(state.address[2], 0x4000);
|
||||
XCTAssertEqual(*_machine->ram_at(0x3000), 0x2200);
|
||||
XCTAssertEqual(*_machine->ram_at(0x4000), 0x1900);
|
||||
}
|
||||
|
||||
- (void)testABCDPredecSame {
|
||||
_machine->set_program({
|
||||
0xc309, // ABCD -(A1), -(A1)
|
||||
});
|
||||
*_machine->ram_at(0x3000) = 0x19a2;
|
||||
|
||||
auto state = _machine->get_processor_state();
|
||||
state.address[1] = 0x3002;
|
||||
state.status = Flag::Extend;
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssert(state.status & Flag::Carry);
|
||||
XCTAssert(state.status & Flag::Extend);
|
||||
XCTAssertEqual(state.address[1], 0x3000);
|
||||
XCTAssertEqual(*_machine->ram_at(0x3000), 0x22a2);
|
||||
}
|
||||
|
||||
// MARK: NBCD
|
||||
|
||||
- (void)performNBCDd1:(uint32_t)d1 ccr:(uint8_t)ccr {
|
||||
_machine->set_program({
|
||||
0x4801 // NBCD D1
|
||||
});
|
||||
auto state = _machine->get_processor_state();
|
||||
state.status |= ccr;
|
||||
state.data[1] = d1;
|
||||
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
XCTAssertEqual(6, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
- (void)testNBCD_Dn {
|
||||
[self performNBCDd1:0x7a ccr:0];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.data[1], 0x20);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Overflow);
|
||||
}
|
||||
|
||||
- (void)testNBCD_Dn_extend {
|
||||
[self performNBCDd1:0x1234567a ccr:Flag::Extend | Flag::Zero];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.data[1], 0x1234561f);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Overflow);
|
||||
}
|
||||
|
||||
- (void)testNBCD_Dn_zero {
|
||||
[self performNBCDd1:0x12345600 ccr:Flag::Zero];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.data[1], 0x12345600);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Zero);
|
||||
}
|
||||
|
||||
- (void)testNBCD_Dn_negative {
|
||||
[self performNBCDd1:0x123456ff ccr:Flag::Extend | Flag::Zero];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.data[1], 0x1234569a);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Negative);
|
||||
}
|
||||
|
||||
- (void)testNBCD_Dn_XXXw {
|
||||
_machine->set_program({
|
||||
0x4838, 0x3000 // NBCD ($3000).w
|
||||
});
|
||||
*_machine->ram_at(0x3000) = 0x0100;
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(16, _machine->get_cycle_count());
|
||||
XCTAssertEqual(*_machine->ram_at(0x3000), 0x9900);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Negative);
|
||||
}
|
||||
|
||||
// MARK: SBCD
|
||||
|
||||
- (void)performSBCDd1:(uint32_t)d1 d2:(uint32_t)d2 ccr:(uint8_t)ccr {
|
||||
_machine->set_program({
|
||||
0x8302 // SBCD D2, D1
|
||||
});
|
||||
auto state = _machine->get_processor_state();
|
||||
state.status |= ccr;
|
||||
state.data[1] = d1;
|
||||
state.data[2] = d2;
|
||||
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssertEqual(6, _machine->get_cycle_count());
|
||||
XCTAssertEqual(state.data[2], d2);
|
||||
}
|
||||
|
||||
- (void)testSBCD_Dn {
|
||||
[self performSBCDd1:0x12345689 d2:0xf745ff78 ccr:Flag::Zero];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.data[1], 0x12345611);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, 0);
|
||||
}
|
||||
|
||||
- (void)testSBCD_Dn_zero {
|
||||
[self performSBCDd1:0x123456ff d2:0xf745ffff ccr:Flag::Zero];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.data[1], 0x12345600);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Zero);
|
||||
}
|
||||
|
||||
- (void)testSBCD_Dn_negative {
|
||||
[self performSBCDd1:0x12345634 d2:0xf745ff45 ccr:Flag::Extend];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.data[1], 0x12345688);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Negative);
|
||||
}
|
||||
|
||||
- (void)testSBCD_Dn_overflow {
|
||||
[self performSBCDd1:0x123456a9 d2:0xf745ffff ccr:Flag::Extend];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.data[1], 0x12345643);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Carry | Flag::Overflow);
|
||||
}
|
||||
|
||||
- (void)testSBCD_Dn_PreDec {
|
||||
_machine->set_program({
|
||||
0x830a // SBCD -(A2), -(A1)
|
||||
});
|
||||
*_machine->ram_at(0x3000) = 0xa200;
|
||||
*_machine->ram_at(0x4000) = 0x1900;
|
||||
auto state = _machine->get_processor_state();
|
||||
state.address[1] = 0x3001;
|
||||
state.address[2] = 0x4001;
|
||||
state.status |= Flag::Extend;
|
||||
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssertEqual(18, _machine->get_cycle_count());
|
||||
XCTAssertEqual(*_machine->ram_at(0x3000), 0x8200);
|
||||
XCTAssertEqual(*_machine->ram_at(0x4000), 0x1900);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Negative);
|
||||
}
|
||||
|
||||
@end
|
||||
1502
OSBindings/Mac/Clock SignalTests/68000BitwiseTests.mm
Normal file
1502
OSBindings/Mac/Clock SignalTests/68000BitwiseTests.mm
Normal file
File diff suppressed because it is too large
Load Diff
557
OSBindings/Mac/Clock SignalTests/68000ControlFlowTests.mm
Normal file
557
OSBindings/Mac/Clock SignalTests/68000ControlFlowTests.mm
Normal file
@@ -0,0 +1,557 @@
|
||||
//
|
||||
// 68000ControlFlowTests.m
|
||||
// Clock SignalTests
|
||||
//
|
||||
// Created by Thomas Harte on 28/06/2019.
|
||||
//
|
||||
// Largely ported from the tests of the Portable 68k Emulator.
|
||||
//
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include "TestRunner68000.hpp"
|
||||
|
||||
|
||||
@interface M68000ControlFlowTests : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation M68000ControlFlowTests {
|
||||
std::unique_ptr<RAM68000> _machine;
|
||||
}
|
||||
|
||||
- (void)setUp {
|
||||
_machine.reset(new RAM68000());
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
_machine.reset();
|
||||
}
|
||||
|
||||
// MARK: Bcc
|
||||
|
||||
- (void)performBccb:(uint16_t)opcode {
|
||||
_machine->set_program({
|
||||
uint16_t(opcode | 6) // Bcc.b +6
|
||||
});
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
}
|
||||
|
||||
- (void)performBccw:(uint16_t)opcode {
|
||||
_machine->set_program({
|
||||
opcode, 0x0006 // Bcc.w +6
|
||||
});
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
}
|
||||
|
||||
- (void)testBHIb {
|
||||
[self performBccb:0x6200];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x1008 + 4);
|
||||
XCTAssertEqual(_machine->get_cycle_count(), 10);
|
||||
}
|
||||
|
||||
- (void)testBLOb {
|
||||
[self performBccb:0x6500];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x1002 + 4);
|
||||
XCTAssertEqual(_machine->get_cycle_count(), 8);
|
||||
}
|
||||
|
||||
- (void)testBHIw {
|
||||
[self performBccw:0x6200];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x1008 + 4);
|
||||
XCTAssertEqual(_machine->get_cycle_count(), 10);
|
||||
}
|
||||
|
||||
- (void)testBLOw {
|
||||
[self performBccw:0x6500];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x1004 + 4);
|
||||
XCTAssertEqual(_machine->get_cycle_count(), 12);
|
||||
}
|
||||
|
||||
// MARK: BRA
|
||||
|
||||
- (void)testBRAb {
|
||||
_machine->set_program({
|
||||
0x6004 // BRA.b +4
|
||||
});
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x1006 + 4);
|
||||
XCTAssertEqual(_machine->get_cycle_count(), 10);
|
||||
}
|
||||
|
||||
- (void)testBRAw {
|
||||
_machine->set_program({
|
||||
0x6000, 0x0004 // BRA.b +4
|
||||
});
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x1006 + 4);
|
||||
XCTAssertEqual(_machine->get_cycle_count(), 10);
|
||||
}
|
||||
|
||||
// MARK: BSR
|
||||
|
||||
- (void)testBSRw {
|
||||
_machine->set_program({
|
||||
0x6100, 0x0006 // BSR.w $1008
|
||||
});
|
||||
_machine->set_initial_stack_pointer(0x3000);
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x1008 + 4);
|
||||
XCTAssertEqual(state.stack_pointer(), 0x2ffc);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, 0);
|
||||
XCTAssertEqual(*_machine->ram_at(0x2ffc), 0);
|
||||
XCTAssertEqual(*_machine->ram_at(0x2ffe), 0x1004);
|
||||
|
||||
XCTAssertEqual(_machine->get_cycle_count(), 18);
|
||||
}
|
||||
|
||||
- (void)testBSRb {
|
||||
_machine->set_program({
|
||||
0x6106 // BSR.b $1008
|
||||
});
|
||||
_machine->set_initial_stack_pointer(0x3000);
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x1008 + 4);
|
||||
XCTAssertEqual(state.stack_pointer(), 0x2ffc);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, 0);
|
||||
XCTAssertEqual(*_machine->ram_at(0x2ffc), 0);
|
||||
XCTAssertEqual(*_machine->ram_at(0x2ffe), 0x1002);
|
||||
|
||||
XCTAssertEqual(_machine->get_cycle_count(), 18);
|
||||
}
|
||||
|
||||
// MARK: CHK
|
||||
|
||||
- (void)performCHKd1:(uint32_t)d1 d2:(uint32_t)d2 {
|
||||
_machine->set_program({
|
||||
0x4581 // CHK D1, D2
|
||||
});
|
||||
auto state = _machine->get_processor_state();
|
||||
state.data[1] = d1;
|
||||
state.data[2] = d2;
|
||||
state.status |= Flag::ConditionCodes;
|
||||
|
||||
_machine->set_initial_stack_pointer(0);
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.data[1], d1);
|
||||
XCTAssertEqual(state.data[2], d2);
|
||||
}
|
||||
|
||||
- (void)testCHK_1111v1111 {
|
||||
[self performCHKd1:0x1111 d2:0x1111];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x1002 + 4);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend);
|
||||
XCTAssertEqual(10, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
- (void)testCHK_1111v0000 {
|
||||
[self performCHKd1:0x1111 d2:0x0000];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x1002 + 4);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Zero);
|
||||
XCTAssertEqual(10, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
- (void)testCHK_8000v8001 {
|
||||
[self performCHKd1:0x8000 d2:0x8001];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertNotEqual(state.program_counter, 0x1002 + 4);
|
||||
XCTAssertEqual(state.stack_pointer(), 0xfffffffa);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend);
|
||||
XCTAssertEqual(42, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
- (void)testCHK_8000v8000 {
|
||||
[self performCHKd1:0x8000 d2:0x8000];
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertNotEqual(state.program_counter, 0x1002 + 4);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::Extend | Flag::Negative);
|
||||
XCTAssertEqual(44, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
// MARK: DBcc
|
||||
|
||||
- (void)performDBccTestOpcode:(uint16_t)opcode status:(uint16_t)status d2Outcome:(uint32_t)d2Output {
|
||||
_machine->set_program({
|
||||
opcode, 0x0008 // DBcc D2, +8
|
||||
});
|
||||
auto state = _machine->get_processor_state();
|
||||
state.status = status;
|
||||
state.data[2] = 1;
|
||||
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.data[2], d2Output);
|
||||
XCTAssertEqual(state.status, status);
|
||||
}
|
||||
|
||||
- (void)testDBT {
|
||||
[self performDBccTestOpcode:0x50ca status:0 d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBF {
|
||||
[self performDBccTestOpcode:0x51ca status:0 d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBHI {
|
||||
[self performDBccTestOpcode:0x52ca status:0 d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBHI_Carry {
|
||||
[self performDBccTestOpcode:0x52ca status:Flag::Carry d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBHI_Zero {
|
||||
[self performDBccTestOpcode:0x52ca status:Flag::Zero d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBLS_CarryOverflow {
|
||||
[self performDBccTestOpcode:0x53ca status:Flag::Carry | Flag::Overflow d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBLS_Carry {
|
||||
[self performDBccTestOpcode:0x53ca status:Flag::Carry d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBLS_Overflow {
|
||||
[self performDBccTestOpcode:0x53ca status:Flag::Overflow d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBCC_Carry {
|
||||
[self performDBccTestOpcode:0x54ca status:Flag::Carry d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBCC {
|
||||
[self performDBccTestOpcode:0x54ca status:0 d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBCS {
|
||||
[self performDBccTestOpcode:0x55ca status:0 d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBCS_Carry {
|
||||
[self performDBccTestOpcode:0x55ca status:Flag::Carry d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBNE {
|
||||
[self performDBccTestOpcode:0x56ca status:0 d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBNE_Zero {
|
||||
[self performDBccTestOpcode:0x56ca status:Flag::Zero d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBEQ {
|
||||
[self performDBccTestOpcode:0x57ca status:0 d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBEQ_Zero {
|
||||
[self performDBccTestOpcode:0x57ca status:Flag::Zero d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBVC {
|
||||
[self performDBccTestOpcode:0x58ca status:0 d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBVC_Overflow {
|
||||
[self performDBccTestOpcode:0x58ca status:Flag::Overflow d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBVS {
|
||||
[self performDBccTestOpcode:0x59ca status:0 d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBVS_Overflow {
|
||||
[self performDBccTestOpcode:0x59ca status:Flag::Overflow d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBPL {
|
||||
[self performDBccTestOpcode:0x5aca status:0 d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBPL_Negative {
|
||||
[self performDBccTestOpcode:0x5aca status:Flag::Negative d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBMI {
|
||||
[self performDBccTestOpcode:0x5bca status:0 d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBMI_Negative {
|
||||
[self performDBccTestOpcode:0x5bca status:Flag::Negative d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBGE_NegativeOverflow {
|
||||
[self performDBccTestOpcode:0x5cca status:Flag::Negative | Flag::Overflow d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBGE {
|
||||
[self performDBccTestOpcode:0x5cca status:0 d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBGE_Negative {
|
||||
[self performDBccTestOpcode:0x5cca status:Flag::Negative d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBGE_Overflow {
|
||||
[self performDBccTestOpcode:0x5cca status:Flag::Overflow d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBLT_NegativeOverflow {
|
||||
[self performDBccTestOpcode:0x5dca status:Flag::Negative | Flag::Overflow d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBLT {
|
||||
[self performDBccTestOpcode:0x5dca status:0 d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBLT_Negative {
|
||||
[self performDBccTestOpcode:0x5dca status:Flag::Negative d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBLT_Overflow {
|
||||
[self performDBccTestOpcode:0x5dca status:Flag::Overflow d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBGT {
|
||||
[self performDBccTestOpcode:0x5eca status:0 d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBGT_ZeroNegativeOverflow {
|
||||
[self performDBccTestOpcode:0x5eca status:Flag::Zero | Flag::Negative | Flag::Overflow d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBGT_NegativeOverflow {
|
||||
[self performDBccTestOpcode:0x5eca status:Flag::Negative | Flag::Overflow d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBGT_Zero {
|
||||
[self performDBccTestOpcode:0x5eca status:Flag::Zero d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBLE {
|
||||
[self performDBccTestOpcode:0x5fca status:0 d2Outcome:0];
|
||||
}
|
||||
|
||||
- (void)testDBLE_Zero {
|
||||
[self performDBccTestOpcode:0x5fca status:Flag::Zero d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBLE_Negative {
|
||||
[self performDBccTestOpcode:0x5fca status:Flag::Negative d2Outcome:1];
|
||||
}
|
||||
|
||||
- (void)testDBLE_NegativeOverflow {
|
||||
[self performDBccTestOpcode:0x5fca status:Flag::Negative | Flag::Overflow d2Outcome:0];
|
||||
}
|
||||
|
||||
/* Further DBF tests omitted; they seemed to be duplicative, assuming I'm not suffering a failure of comprehension. */
|
||||
|
||||
// MARK: JMP
|
||||
|
||||
- (void)testJMP_A1 {
|
||||
_machine->set_program({
|
||||
0x4ed1 // JMP (A1)
|
||||
});
|
||||
|
||||
auto state = _machine->get_processor_state();
|
||||
state.address[1] = 0x3000;
|
||||
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.address[1], 0x3000);
|
||||
XCTAssertEqual(state.program_counter, 0x3000 + 4);
|
||||
XCTAssertEqual(8, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
- (void)testJMP_PC {
|
||||
_machine->set_program({
|
||||
0x4efa, 0x000a // JMP PC+a (i.e. to 0x100c)
|
||||
});
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x100c + 4);
|
||||
XCTAssertEqual(10, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
// MARK: JSR
|
||||
|
||||
- (void)testJSR_PC {
|
||||
_machine->set_program({
|
||||
0x4eba, 0x000a // JSR (+a)PC ; JSR to $100c
|
||||
});
|
||||
_machine->set_initial_stack_pointer(0x2000);
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.stack_pointer(), 0x1ffc);
|
||||
XCTAssertEqual(state.program_counter, 0x100c + 4);
|
||||
XCTAssertEqual(*_machine->ram_at(0x1ffc), 0x0000);
|
||||
XCTAssertEqual(*_machine->ram_at(0x1ffe), 0x1004);
|
||||
XCTAssertEqual(18, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
- (void)testJSR_XXXl {
|
||||
_machine->set_program({
|
||||
0x4eb9, 0x0000, 0x1008 // JSR ($1008).l
|
||||
});
|
||||
_machine->set_initial_stack_pointer(0x2000);
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.stack_pointer(), 0x1ffc);
|
||||
XCTAssertEqual(state.program_counter, 0x1008 + 4);
|
||||
XCTAssertEqual(*_machine->ram_at(0x1ffc), 0x0000);
|
||||
XCTAssertEqual(*_machine->ram_at(0x1ffe), 0x1006);
|
||||
XCTAssertEqual(20, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
// MARK: NOP
|
||||
|
||||
- (void)testNOP {
|
||||
_machine->set_program({
|
||||
0x4e71 // NOP
|
||||
});
|
||||
_machine->run_for_instructions(1);
|
||||
XCTAssertEqual(4, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
// MARK: RTR
|
||||
|
||||
- (void)testRTR {
|
||||
_machine->set_program({
|
||||
0x4e77 // RTR
|
||||
});
|
||||
_machine->set_initial_stack_pointer(0x2000);
|
||||
*_machine->ram_at(0x2000) = 0x7fff;
|
||||
*_machine->ram_at(0x2002) = 0;
|
||||
*_machine->ram_at(0x2004) = 0xc;
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.stack_pointer(), 0x2006);
|
||||
XCTAssertEqual(state.program_counter, 0x10);
|
||||
XCTAssertEqual(state.status & Flag::ConditionCodes, Flag::ConditionCodes);
|
||||
XCTAssertEqual(20, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
// MARK: RTS
|
||||
|
||||
- (void)testRTS {
|
||||
_machine->set_program({
|
||||
0x4e75 // RTS
|
||||
});
|
||||
_machine->set_initial_stack_pointer(0x2000);
|
||||
*_machine->ram_at(0x2000) = 0x0000;
|
||||
*_machine->ram_at(0x2002) = 0x000c;
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.stack_pointer(), 0x2004);
|
||||
XCTAssertEqual(state.program_counter, 0x000c + 4);
|
||||
XCTAssertEqual(16, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
// MARK: TRAP
|
||||
|
||||
- (void)testTRAP {
|
||||
_machine->set_program({
|
||||
0x4e41 // TRAP #1
|
||||
});
|
||||
auto state = _machine->get_processor_state();
|
||||
state.status = 0x700;
|
||||
state.user_stack_pointer = 0x200;
|
||||
state.supervisor_stack_pointer = 0x206;
|
||||
*_machine->ram_at(0x84) = 0xfffe;
|
||||
*_machine->ram_at(0xfffe) = 0x4e71;
|
||||
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.status, 0x2700);
|
||||
XCTAssertEqual(*_machine->ram_at(0x200), 0x700);
|
||||
XCTAssertEqual(*_machine->ram_at(0x202), 0x0000);
|
||||
XCTAssertEqual(*_machine->ram_at(0x204), 0x1002);
|
||||
XCTAssertEqual(state.supervisor_stack_pointer, 0x200);
|
||||
XCTAssertEqual(34, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
// MARK: TRAPV
|
||||
|
||||
- (void)testTRAPV_taken {
|
||||
_machine->set_program({
|
||||
0x4e76 // TRAPV
|
||||
});
|
||||
_machine->set_initial_stack_pointer(0x206);
|
||||
|
||||
auto state = _machine->get_processor_state();
|
||||
state.status = 0x702;
|
||||
state.supervisor_stack_pointer = 0x206;
|
||||
*_machine->ram_at(0x1e) = 0xfffe;
|
||||
*_machine->ram_at(0xfffe) = 0x4e71;
|
||||
|
||||
_machine->set_processor_state(state);
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.status, 0x2702);
|
||||
XCTAssertEqual(state.stack_pointer(), 0x200);
|
||||
XCTAssertEqual(*_machine->ram_at(0x202), 0x0000);
|
||||
XCTAssertEqual(*_machine->ram_at(0x204), 0x1002);
|
||||
XCTAssertEqual(*_machine->ram_at(0x200), 0x702);
|
||||
XCTAssertEqual(34, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
- (void)testTRAPV_untaken {
|
||||
_machine->set_program({
|
||||
0x4e76 // TRAPV
|
||||
});
|
||||
|
||||
_machine->run_for_instructions(1);
|
||||
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssertEqual(state.program_counter, 0x1002 + 4);
|
||||
XCTAssertEqual(4, _machine->get_cycle_count());
|
||||
}
|
||||
|
||||
@end
|
||||
1250
OSBindings/Mac/Clock SignalTests/68000MoveTests.mm
Normal file
1250
OSBindings/Mac/Clock SignalTests/68000MoveTests.mm
Normal file
File diff suppressed because it is too large
Load Diff
1122
OSBindings/Mac/Clock SignalTests/68000RollShiftTests.mm
Normal file
1122
OSBindings/Mac/Clock SignalTests/68000RollShiftTests.mm
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user