mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
1261 Commits
2018-07-11
...
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 | ||
|
|
5b56ad0d78 | ||
|
|
bee0d09877 | ||
|
|
42d8d187b3 | ||
|
|
d97348dd38 | ||
|
|
e1ebb7ce9c | ||
|
|
47dd8ad069 | ||
|
|
6a55d75b3d | ||
|
|
d5b4ddd9e5 | ||
|
|
9c8a2265b5 | ||
|
|
84d7157dfb | ||
|
|
ddce4fb46b | ||
|
|
1ccee036c4 | ||
|
|
ef085e3f93 | ||
|
|
3862a93ff9 | ||
|
|
fc21fbd1f1 | ||
|
|
903f9b5240 | ||
|
|
816ad0a94c | ||
|
|
0536697d8f | ||
|
|
0dbd8a667d | ||
|
|
56e691f256 | ||
|
|
b0503efa3d | ||
|
|
b81e59fd8f | ||
|
|
d2da55aa03 | ||
|
|
28e69152d8 | ||
|
|
d122535a65 | ||
|
|
c8c24f81c8 | ||
|
|
db078c7363 | ||
|
|
6b4f6971de | ||
|
|
79707a3c66 | ||
|
|
694783efe9 | ||
|
|
68c5474e36 | ||
|
|
cd055a0298 | ||
|
|
8f2abab0d9 | ||
|
|
4c5dee866b | ||
|
|
7030abca97 | ||
|
|
c7c21a7e2c | ||
|
|
b23e10e261 | ||
|
|
16731661e8 | ||
|
|
7bb90c78d9 | ||
|
|
a6e61ef83b | ||
|
|
d4134cd0d8 | ||
|
|
c775a6c0f8 | ||
|
|
2f9e825728 | ||
|
|
2f491b5be1 | ||
|
|
de7ebead23 | ||
|
|
c0c4704419 | ||
|
|
ec14750ff1 | ||
|
|
e43de5f1ba | ||
|
|
080f949f89 | ||
|
|
9f6956bd87 | ||
|
|
ddf5e1632d | ||
|
|
40bfde41cb | ||
|
|
e0751af56d | ||
|
|
3979faf43b | ||
|
|
878b480a44 | ||
|
|
b35b6b2ba8 | ||
|
|
d0b967ce53 | ||
|
|
e5addb27ec | ||
|
|
ac8d43cc4a | ||
|
|
40ee215b1b | ||
|
|
6c1d94beaa | ||
|
|
6b2e1fe62b | ||
|
|
8ecf885629 | ||
|
|
6d76b7cd94 | ||
|
|
7bd721f334 | ||
|
|
7939897622 | ||
|
|
77bebd4a65 | ||
|
|
5d68a5bdd0 | ||
|
|
3e0b5433b9 | ||
|
|
ec8f1157c8 | ||
|
|
037cbd534e | ||
|
|
208ef70e31 | ||
|
|
2fa4c59523 | ||
|
|
cda0a2de79 | ||
|
|
008f50832c | ||
|
|
c94acb1ca2 | ||
|
|
d341f98b09 | ||
|
|
e35a3ab566 | ||
|
|
b3b4b7cf0c | ||
|
|
1cd6d58f17 | ||
|
|
eecd4417e7 | ||
|
|
21908dfcef | ||
|
|
75987f64ec | ||
|
|
798cc58f76 | ||
|
|
6ba1194d74 | ||
|
|
e5f75b5df2 | ||
|
|
b75ad3def2 | ||
|
|
10c98f0a15 | ||
|
|
caf72afcb4 | ||
|
|
687e0b376e | ||
|
|
122857e5b5 | ||
|
|
5002290428 | ||
|
|
d09ac3384f | ||
|
|
b6a4a7e0a5 | ||
|
|
c87994336c | ||
|
|
85ad490089 | ||
|
|
73e32a9c76 | ||
|
|
a321ff3037 | ||
|
|
68d6feaa03 | ||
|
|
74e1a9a621 | ||
|
|
097bc7055e | ||
|
|
6a43fc5df0 | ||
|
|
312f38906b | ||
|
|
f0ec9fa5d2 | ||
|
|
20b4896940 | ||
|
|
6a93d2d006 | ||
|
|
ae0bc7e7aa | ||
|
|
a8acadbe13 | ||
|
|
727f2e2ba0 | ||
|
|
a6683cb9b8 | ||
|
|
5ceb711bd3 | ||
|
|
4748b09721 | ||
|
|
d593796dae | ||
|
|
ef0dbc2a41 | ||
|
|
6c49953115 | ||
|
|
55290f4dad | ||
|
|
f373a3fbb1 | ||
|
|
bb03d2f2ad | ||
|
|
82922aa2c7 | ||
|
|
7aec5be61a | ||
|
|
2ef6d4327c | ||
|
|
cc95e587db | ||
|
|
e89e55a9bb | ||
|
|
7c2c243985 | ||
|
|
25a1f23fc0 | ||
|
|
27541196cc | ||
|
|
5d9521fcb9 | ||
|
|
ccb52fb625 | ||
|
|
028e530232 | ||
|
|
906a2ff6eb | ||
|
|
248a8efd2f | ||
|
|
c392c819c1 | ||
|
|
e9d9ff0da0 | ||
|
|
46d756d298 | ||
|
|
fd0ffc7085 | ||
|
|
601961deeb | ||
|
|
557a2a0ddf | ||
|
|
b723740f64 | ||
|
|
6be46ae921 | ||
|
|
a25470ee41 | ||
|
|
fd579a019b | ||
|
|
e39ecf59ef | ||
|
|
5f90941e4e | ||
|
|
64465f97b6 | ||
|
|
aa22af6f05 | ||
|
|
a6383247fc | ||
|
|
d45c2a1f28 | ||
|
|
61a63a673c | ||
|
|
5618288459 | ||
|
|
b69ac4ec2f | ||
|
|
f3174069fa | ||
|
|
cd1e796093 | ||
|
|
dd4af4f0df | ||
|
|
76656fab23 | ||
|
|
cf49603a9e | ||
|
|
6c92853461 | ||
|
|
6a62cf9146 | ||
|
|
4fa6bc0ad1 | ||
|
|
95685749ad | ||
|
|
d7c0f0c804 | ||
|
|
6b42b92930 | ||
|
|
f4764ea680 | ||
|
|
538c57664f | ||
|
|
a66a20f7fe | ||
|
|
d4ac79b0af | ||
|
|
a5a3769a0f | ||
|
|
dc4b5cc37d | ||
|
|
ee89be6730 | ||
|
|
770d7e90e9 | ||
|
|
b9aca39eb0 | ||
|
|
c0454ff101 | ||
|
|
a697a2e4f6 | ||
|
|
396cf72029 | ||
|
|
bfe9704829 | ||
|
|
43ee540233 | ||
|
|
817aa186c2 | ||
|
|
38ffc4fdb3 | ||
|
|
f12d734957 | ||
|
|
a70991d50e | ||
|
|
4c00456166 | ||
|
|
26219213d7 | ||
|
|
97c5ee6c0a | ||
|
|
75bc0e451d | ||
|
|
6496b6313c | ||
|
|
c5d9bf2c12 | ||
|
|
8f05560dd7 | ||
|
|
06c0c64c1a | ||
|
|
c173777d12 | ||
|
|
16dfeb3fc8 | ||
|
|
5a31891048 | ||
|
|
8b37496447 | ||
|
|
8f6664f0d7 | ||
|
|
15b1176841 | ||
|
|
3eab1f8f7c | ||
|
|
9dff13cbbf | ||
|
|
a47de9a884 | ||
|
|
8a699b6072 | ||
|
|
87df8b9e85 | ||
|
|
91b19c5c70 | ||
|
|
0487580a1a | ||
|
|
3dca836571 | ||
|
|
6ba02c44d0 | ||
|
|
bf3ab4e260 | ||
|
|
02f9cada43 | ||
|
|
654a19ea15 | ||
|
|
ecb5504bd1 | ||
|
|
2adf3d353e | ||
|
|
3045e85004 | ||
|
|
e9d1afd515 | ||
|
|
833ab7945b | ||
|
|
0af1d668a6 | ||
|
|
0ac62e3805 | ||
|
|
938d09f34a | ||
|
|
dce52d740d | ||
|
|
3ae333fa84 | ||
|
|
d5af1f3948 | ||
|
|
0ba3ae53ab | ||
|
|
be12d78c83 | ||
|
|
b70227ac1b | ||
|
|
6d277fecd5 | ||
|
|
491817d85c | ||
|
|
20faf4e477 | ||
|
|
4fe5c7c24e | ||
|
|
36bf640c6f | ||
|
|
7881e40e0b | ||
|
|
55da1e9c0f | ||
|
|
9799aa0975 | ||
|
|
1effb97b74 | ||
|
|
eb28095041 | ||
|
|
014da41471 | ||
|
|
0446e350d3 | ||
|
|
05fb7db147 | ||
|
|
f6562de325 | ||
|
|
b40211d2c0 | ||
|
|
da4d883321 | ||
|
|
373820f080 | ||
|
|
6e517983b9 | ||
|
|
b9a752fda1 | ||
|
|
f65d80b7d1 | ||
|
|
4701aa149a | ||
|
|
0d051502e2 | ||
|
|
2e28a8e51c | ||
|
|
4af0b74a42 | ||
|
|
d1fc39d6e5 | ||
|
|
4f0d324a6b | ||
|
|
8652d8b23d | ||
|
|
e02aa885d8 | ||
|
|
1fc9356796 | ||
|
|
bb09762029 | ||
|
|
05a5c7120e | ||
|
|
521d603902 | ||
|
|
916710353a | ||
|
|
53b00dea3f | ||
|
|
0587b9f257 | ||
|
|
9621ba59ae | ||
|
|
5accd8cf08 | ||
|
|
38c130df2b | ||
|
|
8730ffb4e2 | ||
|
|
a8645f80bf | ||
|
|
278585fd94 | ||
|
|
d61c3a9442 | ||
|
|
2cdeaa2575 | ||
|
|
286783e880 | ||
|
|
f7b0f1af70 | ||
|
|
f69cb28933 | ||
|
|
e3fd63b2d7 | ||
|
|
6cb956d1d6 | ||
|
|
00e7958a97 | ||
|
|
cba8e6814f | ||
|
|
2f995eb622 | ||
|
|
90fbad0f1c | ||
|
|
2cbd28478d | ||
|
|
7eeefd2602 | ||
|
|
1331457314 | ||
|
|
7855145ebd | ||
|
|
6ab30e9cac | ||
|
|
027e9c7816 | ||
|
|
7c65cfd932 | ||
|
|
883680731a | ||
|
|
fb3171f366 | ||
|
|
c07f9fed99 | ||
|
|
616777517d | ||
|
|
b3f1677da5 | ||
|
|
16f08eb654 | ||
|
|
a38974ef2e | ||
|
|
725b364bbc | ||
|
|
30b99f0049 | ||
|
|
b61de65b43 | ||
|
|
0822c96ce0 | ||
|
|
3b164e5ffe | ||
|
|
d5f1e76707 | ||
|
|
f49718e94b | ||
|
|
b18db78cce | ||
|
|
c39fd17e54 | ||
|
|
78fff5bdd9 | ||
|
|
6fff514901 | ||
|
|
f9a6c00493 | ||
|
|
fa77d81813 | ||
|
|
f0b6c406ff | ||
|
|
c365cca38a | ||
|
|
4cd65eab5c | ||
|
|
2ee360e6ba | ||
|
|
9bc09046c0 | ||
|
|
10d9cbdeb1 | ||
|
|
57f03e660c | ||
|
|
512f085891 | ||
|
|
6a2db52adb | ||
|
|
34e13d0d4d | ||
|
|
da00c832f5 | ||
|
|
8ff265c3a1 | ||
|
|
0278d5b61c | ||
|
|
1fc88c4eff | ||
|
|
58ca74c68a | ||
|
|
b4f871a2ef | ||
|
|
0f7bf6d6c6 | ||
|
|
5dfe7d8596 | ||
|
|
231009b901 | ||
|
|
1c5f939aea | ||
|
|
c1e6406fc9 | ||
|
|
d66979c68f | ||
|
|
6c09abc6cb | ||
|
|
9e52ead09a | ||
|
|
9ab0c54426 | ||
|
|
f6af6778ab | ||
|
|
6a94dda60d | ||
|
|
82b7944599 | ||
|
|
52e02db5c8 | ||
|
|
9a933993f5 | ||
|
|
062b2ae8d3 | ||
|
|
9f69dbf31a | ||
|
|
63fb3f03d1 | ||
|
|
2e379b0834 | ||
|
|
f00f6c8c23 | ||
|
|
50e23f4a2e | ||
|
|
acdc84e08c | ||
|
|
c128ddb549 | ||
|
|
dccf17e770 | ||
|
|
2d8ab72e22 | ||
|
|
748366c70e | ||
|
|
7a74fe2ff7 | ||
|
|
e410302237 | ||
|
|
bca2161a05 | ||
|
|
5f789092be | ||
|
|
6975ed22c0 | ||
|
|
24644f1dd1 | ||
|
|
3bead07043 | ||
|
|
ee20e42372 | ||
|
|
df411b4ede | ||
|
|
bfb9d8ccb6 | ||
|
|
338aec2930 | ||
|
|
e6510dc87b | ||
|
|
76f3b9f6ba | ||
|
|
7830cda912 | ||
|
|
aac97a8983 | ||
|
|
ca26dfcd61 | ||
|
|
858721a7a5 | ||
|
|
89db1d6a6a | ||
|
|
de4e5c40aa | ||
|
|
05248ab990 | ||
|
|
252f47a425 | ||
|
|
be52b31b5c | ||
|
|
23c3fa6993 | ||
|
|
499fc62187 | ||
|
|
1dd5272190 | ||
|
|
5361120353 | ||
|
|
60bab8fdf1 | ||
|
|
cc99b0f532 | ||
|
|
91aa8f9295 | ||
|
|
e9328d819e | ||
|
|
48ece623e7 | ||
|
|
23191efc05 | ||
|
|
0d8af010b6 | ||
|
|
7b9bb772ca | ||
|
|
f7e211c245 | ||
|
|
43bcb6415b | ||
|
|
15ec4aaa43 | ||
|
|
364859467f | ||
|
|
35c2e74af8 | ||
|
|
19482a563f | ||
|
|
2e4c4c3e91 | ||
|
|
7515fa8a98 | ||
|
|
5b9e7213dd | ||
|
|
2253341904 | ||
|
|
e155dc8d6e | ||
|
|
00b2db4fb9 | ||
|
|
f59386f523 | ||
|
|
9683c8f664 | ||
|
|
38a1fde3bf | ||
|
|
40c7a63fb5 | ||
|
|
d9e65cd758 | ||
|
|
e511261b04 | ||
|
|
0d01346ad4 | ||
|
|
e7f4babf41 | ||
|
|
a29a8e292b | ||
|
|
a8b116e217 | ||
|
|
e582b4c8ca | ||
|
|
3b70dbfebe | ||
|
|
868cd5cb09 | ||
|
|
dec18d9acc | ||
|
|
a7508bc2ae | ||
|
|
a38639d099 | ||
|
|
c6e94bc2a6 | ||
|
|
12e9478a81 | ||
|
|
09dafb1a79 | ||
|
|
3358c07107 | ||
|
|
36ff2105fb | ||
|
|
1739d18433 | ||
|
|
61272cfe20 | ||
|
|
31b048f966 | ||
|
|
7e29d49451 | ||
|
|
5445081c96 | ||
|
|
d791facc87 | ||
|
|
21a9bd927a | ||
|
|
d73d3b4480 | ||
|
|
7b9c1bb69c | ||
|
|
224b3163f2 | ||
|
|
fc84ae611e | ||
|
|
22a52bdca2 | ||
|
|
6e9cd5cb21 | ||
|
|
c73445199c | ||
|
|
ab02f82470 | ||
|
|
1e3318816c | ||
|
|
4c8781c762 | ||
|
|
3a3dec92c7 | ||
|
|
5a5fc1ae1a | ||
|
|
8d79a1e381 | ||
|
|
d70f5da94e | ||
|
|
05d4274019 | ||
|
|
afeec09902 | ||
|
|
0526ac2ee2 | ||
|
|
6725ee2190 | ||
|
|
8b661fb90f | ||
|
|
dab7d3db1b | ||
|
|
1cba3d48d9 | ||
|
|
d53b38ec7e | ||
|
|
5d0f47eda2 | ||
|
|
2e04c4442c | ||
|
|
f639cdc8ad | ||
|
|
71ec7624ca | ||
|
|
0599d9602e | ||
|
|
234bef2a88 | ||
|
|
adb574e1cd | ||
|
|
1f491e764e | ||
|
|
114a43a662 | ||
|
|
5547c39c91 | ||
|
|
97a89aaf4d | ||
|
|
61e46399dc | ||
|
|
e802f6ecc2 | ||
|
|
4209f0e044 | ||
|
|
33576aa2c4 | ||
|
|
17bf1a64bf | ||
|
|
f8d46f8f3d | ||
|
|
8787d85e64 | ||
|
|
7f0f17f435 | ||
|
|
0e7f54f375 | ||
|
|
b3bdfa9f46 | ||
|
|
592ec69d36 | ||
|
|
60e00ddd02 | ||
|
|
6806193dc2 | ||
|
|
c35dca783f | ||
|
|
901e0d65b9 | ||
|
|
ddf45a0010 | ||
|
|
1eca4463b3 | ||
|
|
be01203cc1 | ||
|
|
4d1d19a464 | ||
|
|
760817eb3b | ||
|
|
cb47575860 | ||
|
|
434d184503 | ||
|
|
7374c665e8 | ||
|
|
10c930a59d | ||
|
|
60ab6f0c2a | ||
|
|
a13eb351da | ||
|
|
4b91910fab | ||
|
|
f46d52364c | ||
|
|
878c63dcd2 | ||
|
|
261fb3d4f8 | ||
|
|
b63e0cff72 | ||
|
|
5d6e479338 | ||
|
|
90094529a5 | ||
|
|
aed4c0539e | ||
|
|
8b50ab2593 | ||
|
|
95164b79c9 | ||
|
|
6f838fe190 | ||
|
|
bb680b40d8 | ||
|
|
e3f6da6994 | ||
|
|
e46bde35f5 | ||
|
|
32338bea4d | ||
|
|
5c881bd19d | ||
|
|
1a44ef0469 | ||
|
|
ebce9a2e51 | ||
|
|
633af4d404 | ||
|
|
76a73c835c | ||
|
|
c1d1c451ef | ||
|
|
3be30d8c71 | ||
|
|
d4c1244485 | ||
|
|
c61b9dca17 | ||
|
|
39bf682016 | ||
|
|
60ac9b49ea | ||
|
|
a8bb18e2cf | ||
|
|
1852786609 | ||
|
|
31df8c7e91 | ||
|
|
832939f5b7 | ||
|
|
c2d9e1ec81 | ||
|
|
673b915ee8 | ||
|
|
032a62dfff | ||
|
|
f2d78182a3 | ||
|
|
de68e70246 | ||
|
|
e07447eb9a | ||
|
|
5cdeb58571 | ||
|
|
ce14cc8677 | ||
|
|
bcd0479074 | ||
|
|
d72dd8c4ff | ||
|
|
f7ce86fef8 | ||
|
|
55f2fccf5e | ||
|
|
c939a274be | ||
|
|
101fb5d7bf | ||
|
|
3c51e335c3 | ||
|
|
33ea90678c | ||
|
|
11ae2c64ba | ||
|
|
26624d7652 | ||
|
|
85fb4773b0 | ||
|
|
099d66804e | ||
|
|
086596c28e | ||
|
|
3aeb4213fe | ||
|
|
558b96bc05 | ||
|
|
e97cc40a2c | ||
|
|
94503ed771 | ||
|
|
c4f86cc324 | ||
|
|
70c4d6b9b3 | ||
|
|
78c7137427 | ||
|
|
74a2f717b3 | ||
|
|
98bb5bd9f1 | ||
|
|
c91eaaf8da | ||
|
|
a36f37d240 | ||
|
|
c773d3501a | ||
|
|
5810f9b3f9 | ||
|
|
3f56683342 | ||
|
|
16ccbdefd6 | ||
|
|
a533d09fe7 | ||
|
|
e9aaa5bbdf | ||
|
|
ecb26e3281 | ||
|
|
5aa0b17720 | ||
|
|
632b37ecec | ||
|
|
c905de2e40 | ||
|
|
bc2afe69e1 | ||
|
|
894998b163 | ||
|
|
51192d8397 | ||
|
|
3c33ccd730 | ||
|
|
3e35109d63 | ||
|
|
99c770eab4 | ||
|
|
34aa78b7ce | ||
|
|
8cca9c2055 | ||
|
|
85ce21c79f | ||
|
|
d19d949b9c | ||
|
|
1cb3713b84 | ||
|
|
689850d698 | ||
|
|
c572a52049 | ||
|
|
41765e00c4 | ||
|
|
080aa0acc5 | ||
|
|
5e7c46a72a | ||
|
|
5f2b9b2d5a | ||
|
|
5c4506a9db | ||
|
|
55a6431fb3 | ||
|
|
ede2696a77 | ||
|
|
59b9e39022 | ||
|
|
6b2970f2f2 | ||
|
|
6a73fe7d65 | ||
|
|
1362906f94 | ||
|
|
8f4042c4bb | ||
|
|
c05b6397b0 | ||
|
|
8d18808efe | ||
|
|
09950d9414 | ||
|
|
badbbdf155 | ||
|
|
2832792fed | ||
|
|
efa45b9504 | ||
|
|
523749edf8 | ||
|
|
5a0499e8a7 | ||
|
|
258c8b5900 | ||
|
|
24b861f056 | ||
|
|
29f7f4d432 | ||
|
|
21080a1149 | ||
|
|
1d068fd09b | ||
|
|
92065813ef | ||
|
|
3e9ef6b8cb | ||
|
|
c9451a5382 | ||
|
|
2be3b027db | ||
|
|
e339d169c5 | ||
|
|
87001f86ee | ||
|
|
58484e8f37 | ||
|
|
00cb4d26b3 |
6
.editorconfig
Normal file
6
.editorconfig
Normal file
@@ -0,0 +1,6 @@
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -24,13 +24,13 @@ namespace Activity {
|
||||
class Observer {
|
||||
public:
|
||||
/// Announces to the receiver that there is an LED of name @c name.
|
||||
virtual void register_led(const std::string &name) = 0;
|
||||
virtual void register_led(const std::string &name) {}
|
||||
|
||||
/// Announces to the receiver that there is a drive of name @c name.
|
||||
virtual void register_drive(const std::string &name) = 0;
|
||||
virtual void register_drive(const std::string &name) {}
|
||||
|
||||
/// Informs the receiver of the new state of the LED with name @c name.
|
||||
virtual void set_led_status(const std::string &name, bool lit) = 0;
|
||||
virtual void set_led_status(const std::string &name, bool lit) {}
|
||||
|
||||
enum class DriveEvent {
|
||||
StepNormal,
|
||||
@@ -39,11 +39,10 @@ class Observer {
|
||||
};
|
||||
|
||||
/// Informs the receiver that the named event just occurred for the drive with name @c name.
|
||||
virtual void announce_drive_event(const std::string &name, DriveEvent event) = 0;
|
||||
virtual void announce_drive_event(const std::string &name, DriveEvent event) {}
|
||||
|
||||
/// Informs the receiver of the motor-on status of the drive with name @c name.
|
||||
virtual void set_drive_motor_status(const std::string &name, bool is_on) = 0;
|
||||
|
||||
virtual void set_drive_motor_status(const std::string &name, bool is_on) {}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
using namespace Analyser::Dynamic;
|
||||
|
||||
MultiCRTMachine::MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::mutex &machines_mutex) :
|
||||
MultiCRTMachine::MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::recursive_mutex &machines_mutex) :
|
||||
machines_(machines), machines_mutex_(machines_mutex), queues_(machines.size()) {
|
||||
speaker_ = MultiSpeaker::create(machines);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ void MultiCRTMachine::perform_parallel(const std::function<void(::CRTMachine::Ma
|
||||
std::condition_variable condition;
|
||||
std::mutex mutex;
|
||||
{
|
||||
std::lock_guard<std::mutex> machines_lock(machines_mutex_);
|
||||
std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_);
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
outstanding_machines = machines_.size();
|
||||
|
||||
@@ -46,29 +46,18 @@ void MultiCRTMachine::perform_parallel(const std::function<void(::CRTMachine::Ma
|
||||
}
|
||||
|
||||
void MultiCRTMachine::perform_serial(const std::function<void (::CRTMachine::Machine *)> &function) {
|
||||
std::lock_guard<std::mutex> machines_lock(machines_mutex_);
|
||||
std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_);
|
||||
for(const auto &machine: machines_) {
|
||||
CRTMachine::Machine *crt_machine = machine->crt_machine();
|
||||
CRTMachine::Machine *const crt_machine = machine->crt_machine();
|
||||
if(crt_machine) function(crt_machine);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiCRTMachine::setup_output(float aspect_ratio) {
|
||||
perform_serial([=](::CRTMachine::Machine *machine) {
|
||||
machine->setup_output(aspect_ratio);
|
||||
});
|
||||
}
|
||||
void MultiCRTMachine::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
scan_target_ = scan_target;
|
||||
|
||||
void MultiCRTMachine::close_output() {
|
||||
perform_serial([=](::CRTMachine::Machine *machine) {
|
||||
machine->close_output();
|
||||
});
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *MultiCRTMachine::get_crt() {
|
||||
std::lock_guard<std::mutex> machines_lock(machines_mutex_);
|
||||
CRTMachine::Machine *crt_machine = machines_.front()->crt_machine();
|
||||
return crt_machine ? crt_machine->get_crt() : nullptr;
|
||||
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
|
||||
if(crt_machine) crt_machine->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() {
|
||||
@@ -84,6 +73,14 @@ void MultiCRTMachine::run_for(Time::Seconds duration) {
|
||||
}
|
||||
|
||||
void MultiCRTMachine::did_change_machine_order() {
|
||||
if(scan_target_) scan_target_->will_change_owner();
|
||||
|
||||
perform_serial([=](::CRTMachine::Machine *machine) {
|
||||
machine->set_scan_target(nullptr);
|
||||
});
|
||||
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
|
||||
if(crt_machine) crt_machine->set_scan_target(scan_target_);
|
||||
|
||||
if(speaker_) {
|
||||
speaker_->set_new_front_machine(machines_.front().get());
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Dynamic {
|
||||
*/
|
||||
class MultiCRTMachine: public CRTMachine::Machine {
|
||||
public:
|
||||
MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::mutex &machines_mutex);
|
||||
MultiCRTMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines, std::recursive_mutex &machines_mutex);
|
||||
|
||||
/*!
|
||||
Informs the MultiCRTMachine that the order of machines has changed; the MultiCRTMachine
|
||||
@@ -53,19 +53,18 @@ class MultiCRTMachine: public CRTMachine::Machine {
|
||||
}
|
||||
|
||||
// Below is the standard CRTMachine::Machine interface; see there for documentation.
|
||||
void setup_output(float aspect_ratio) override;
|
||||
void close_output() override;
|
||||
Outputs::CRT::CRT *get_crt() override;
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override;
|
||||
Outputs::Speaker::Speaker *get_speaker() override;
|
||||
void run_for(Time::Seconds duration) override;
|
||||
|
||||
private:
|
||||
void run_for(const Cycles cycles) override {}
|
||||
const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_;
|
||||
std::mutex &machines_mutex_;
|
||||
std::recursive_mutex &machines_mutex_;
|
||||
std::vector<Concurrency::AsyncTaskQueue> queues_;
|
||||
MultiSpeaker *speaker_ = nullptr;
|
||||
Delegate *delegate_ = nullptr;
|
||||
Outputs::Display::ScanTarget *scan_target_ = nullptr;
|
||||
|
||||
/*!
|
||||
Performs a parallel for operation across all machines, performing the supplied
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
using namespace Analyser::Dynamic;
|
||||
|
||||
MultiKeyboardMachine::MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) {
|
||||
MultiKeyboardMachine::MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) :
|
||||
keyboard_(machines_) {
|
||||
for(const auto &machine: machines) {
|
||||
KeyboardMachine::Machine *keyboard_machine = machine->keyboard_machine();
|
||||
if(keyboard_machine) machines_.push_back(keyboard_machine);
|
||||
@@ -35,9 +36,34 @@ void MultiKeyboardMachine::type_string(const std::string &string) {
|
||||
}
|
||||
}
|
||||
|
||||
void MultiKeyboardMachine::keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) {
|
||||
Inputs::Keyboard &MultiKeyboardMachine::get_keyboard() {
|
||||
return keyboard_;
|
||||
}
|
||||
|
||||
MultiKeyboardMachine::MultiKeyboard::MultiKeyboard(const std::vector<::KeyboardMachine::Machine *> &machines)
|
||||
: machines_(machines) {
|
||||
for(const auto &machine: machines_) {
|
||||
uint16_t mapped_key = machine->get_keyboard_mapper()->mapped_key_for_key(key);
|
||||
if(mapped_key != KeyNotMapped) machine->set_key_state(mapped_key, is_pressed);
|
||||
observed_keys_.insert(machine->get_keyboard().observed_keys().begin(), machine->get_keyboard().observed_keys().end());
|
||||
is_exclusive_ |= machine->get_keyboard().is_exclusive();
|
||||
}
|
||||
}
|
||||
|
||||
void MultiKeyboardMachine::MultiKeyboard::set_key_pressed(Key key, char value, bool is_pressed) {
|
||||
for(const auto &machine: machines_) {
|
||||
machine->get_keyboard().set_key_pressed(key, value, is_pressed);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiKeyboardMachine::MultiKeyboard::reset_all_keys() {
|
||||
for(const auto &machine: machines_) {
|
||||
machine->get_keyboard().reset_all_keys();
|
||||
}
|
||||
}
|
||||
|
||||
const std::set<Inputs::Keyboard::Key> &MultiKeyboardMachine::MultiKeyboard::observed_keys() {
|
||||
return observed_keys_;
|
||||
}
|
||||
|
||||
bool MultiKeyboardMachine::MultiKeyboard::is_exclusive() {
|
||||
return is_exclusive_;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,25 @@ namespace Dynamic {
|
||||
order of delivered messages.
|
||||
*/
|
||||
class MultiKeyboardMachine: public KeyboardMachine::Machine {
|
||||
private:
|
||||
std::vector<::KeyboardMachine::Machine *> machines_;
|
||||
|
||||
class MultiKeyboard: public Inputs::Keyboard {
|
||||
public:
|
||||
MultiKeyboard(const std::vector<::KeyboardMachine::Machine *> &machines);
|
||||
|
||||
void set_key_pressed(Key key, char value, bool is_pressed) override;
|
||||
void reset_all_keys() override;
|
||||
const std::set<Key> &observed_keys() override;
|
||||
bool is_exclusive() override;
|
||||
|
||||
private:
|
||||
const std::vector<::KeyboardMachine::Machine *> &machines_;
|
||||
std::set<Key> observed_keys_;
|
||||
bool is_exclusive_ = false;
|
||||
};
|
||||
MultiKeyboard keyboard_;
|
||||
|
||||
public:
|
||||
MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
|
||||
|
||||
@@ -32,10 +51,7 @@ class MultiKeyboardMachine: public KeyboardMachine::Machine {
|
||||
void clear_all_keys() override;
|
||||
void set_key_state(uint16_t key, bool is_pressed) override;
|
||||
void type_string(const std::string &) override;
|
||||
void keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) override;
|
||||
|
||||
private:
|
||||
std::vector<::KeyboardMachine::Machine *> machines_;
|
||||
Inputs::Keyboard &get_keyboard() override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#include "MultiMachine.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -58,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();
|
||||
@@ -73,15 +79,13 @@ bool MultiMachine::would_collapse(const std::vector<std::unique_ptr<DynamicMachi
|
||||
}
|
||||
|
||||
void MultiMachine::multi_crt_did_run_machines() {
|
||||
std::lock_guard<std::mutex> machines_lock(machines_mutex_);
|
||||
#ifdef DEBUG
|
||||
std::lock_guard<decltype(machines_mutex_)> machines_lock(machines_mutex_);
|
||||
#ifndef NDEBUG
|
||||
for(const auto &machine: machines_) {
|
||||
CRTMachine::Machine *crt = machine->crt_machine();
|
||||
printf("%0.2f ", crt->get_confidence());
|
||||
crt->print_type();
|
||||
printf("; ");
|
||||
LOGNBR(PADHEX(2) << crt->get_confidence() << " " << crt->debug_type() << "; ");
|
||||
}
|
||||
printf("\n");
|
||||
LOGNBR(std::endl);
|
||||
#endif
|
||||
|
||||
DynamicMachine *front = machines_.front().get();
|
||||
|
||||
@@ -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;
|
||||
@@ -62,7 +63,7 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De
|
||||
void multi_crt_did_run_machines() override;
|
||||
|
||||
std::vector<std::unique_ptr<DynamicMachine>> machines_;
|
||||
std::mutex machines_mutex_;
|
||||
std::recursive_mutex machines_mutex_;
|
||||
|
||||
MultiConfigurable configurable_;
|
||||
MultiCRTMachine crt_machine_;
|
||||
|
||||
@@ -17,6 +17,8 @@ enum class Machine {
|
||||
Atari2600,
|
||||
ColecoVision,
|
||||
Electron,
|
||||
Macintosh,
|
||||
MasterSystem,
|
||||
MSX,
|
||||
Oric,
|
||||
Vic20,
|
||||
|
||||
@@ -18,7 +18,9 @@ namespace AppleII {
|
||||
struct Target: public ::Analyser::Static::Target {
|
||||
enum class Model {
|
||||
II,
|
||||
IIplus
|
||||
IIplus,
|
||||
IIe,
|
||||
EnhancedIIe
|
||||
};
|
||||
enum class DiskController {
|
||||
None,
|
||||
@@ -26,7 +28,7 @@ struct Target: public ::Analyser::Static::Target {
|
||||
ThirteenSector
|
||||
};
|
||||
|
||||
Model model = Model::IIplus;
|
||||
Model model = Model::IIe;
|
||||
DiskController disk_controller = DiskController::None;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,13 +17,8 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
|
||||
// only one mapped item is allowed
|
||||
if(segments.size() != 1) continue;
|
||||
|
||||
// which must be 8, 12, 16, 24 or 32 kb in size
|
||||
const Storage::Cartridge::Cartridge::Segment &segment = segments.front();
|
||||
const std::size_t data_size = segment.data.size();
|
||||
const std::size_t overflow = data_size&8191;
|
||||
if(overflow > 8 && overflow != 512 && (data_size != 12*1024)) continue;
|
||||
if(data_size < 8192) continue;
|
||||
|
||||
// the two bytes that will be first must be 0xaa and 0x55, either way around
|
||||
auto *start = &segment.data[0];
|
||||
@@ -34,19 +29,24 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
if(start[0] == start[1]) continue;
|
||||
|
||||
// probability of a random binary blob that isn't a Coleco ROM proceeding to here is 1 - 1/32768.
|
||||
if(!overflow) {
|
||||
coleco_cartridges.push_back(cartridge);
|
||||
|
||||
// Round up to the next multiple of 8kb if this image is less than 32kb. Otherwise round down if
|
||||
// this image is within a short distance of 32kb.
|
||||
std::vector<Storage::Cartridge::Cartridge::Segment> output_segments;
|
||||
|
||||
size_t target_size;
|
||||
if(data_size >= 32*1024 && data_size < 32*1024 + 512) {
|
||||
target_size = 32 * 1024;
|
||||
} else {
|
||||
// Size down to a multiple of 8kb and apply the start address.
|
||||
std::vector<Storage::Cartridge::Cartridge::Segment> output_segments;
|
||||
|
||||
std::vector<uint8_t> truncated_data;
|
||||
std::vector<uint8_t>::difference_type truncated_size = static_cast<std::vector<uint8_t>::difference_type>(segment.data.size()) & ~8191;
|
||||
truncated_data.insert(truncated_data.begin(), segment.data.begin(), segment.data.begin() + truncated_size);
|
||||
output_segments.emplace_back(0x8000, truncated_data);
|
||||
|
||||
coleco_cartridges.emplace_back(new Storage::Cartridge::Cartridge(output_segments));
|
||||
target_size = data_size + ((8192 - (data_size & 8191)) & 8191);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> truncated_data;
|
||||
truncated_data = segment.data;
|
||||
truncated_data.resize(target_size);
|
||||
output_segments.emplace_back(0x8000, truncated_data);
|
||||
|
||||
coleco_cartridges.emplace_back(new Storage::Cartridge::Cartridge(output_segments));
|
||||
}
|
||||
|
||||
return coleco_cartridges;
|
||||
|
||||
@@ -23,5 +23,4 @@ TargetList GetTargets(const Media &media, const std::string &file_name, TargetPl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif /* StaticAnalyser_hpp */
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "Tape.hpp"
|
||||
#include "Target.hpp"
|
||||
#include "../../../Storage/Cartridge/Encodings/CommodoreROM.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
@@ -80,7 +81,7 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media
|
||||
target->memory_model = Target::MemoryModel::Unexpanded;
|
||||
std::ostringstream string_stream;
|
||||
string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ",";
|
||||
if(files.front().is_basic()) {
|
||||
if(files.front().is_basic()) {
|
||||
string_stream << "0";
|
||||
} else {
|
||||
string_stream << "1";
|
||||
@@ -91,7 +92,7 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media
|
||||
// make a first guess based on loading address
|
||||
switch(files.front().starting_address) {
|
||||
default:
|
||||
printf("Starting address %04x?\n", files.front().starting_address);
|
||||
LOG("Unrecognised loading address for Commodore program: " << PADHEX(4) << files.front().starting_address);
|
||||
case 0x1001:
|
||||
target->memory_model = Target::MemoryModel::Unexpanded;
|
||||
break;
|
||||
|
||||
@@ -63,9 +63,9 @@ class Accessor {
|
||||
#define z(v) (v & 7)
|
||||
|
||||
Instruction::Condition condition_table[] = {
|
||||
Instruction::Condition::NZ, Instruction::Condition::Z,
|
||||
Instruction::Condition::NC, Instruction::Condition::C,
|
||||
Instruction::Condition::PO, Instruction::Condition::PE,
|
||||
Instruction::Condition::NZ, Instruction::Condition::Z,
|
||||
Instruction::Condition::NC, Instruction::Condition::C,
|
||||
Instruction::Condition::PO, Instruction::Condition::PE,
|
||||
Instruction::Condition::P, Instruction::Condition::M
|
||||
};
|
||||
|
||||
|
||||
@@ -285,7 +285,12 @@ Analyser::Static::TargetList Analyser::Static::MSX::GetTargets(const Media &medi
|
||||
}
|
||||
}
|
||||
|
||||
// Region selection: for now, this as simple as:
|
||||
// "If a tape is involved, be European. Otherwise be American (i.e. English, but 60Hz)".
|
||||
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();
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ namespace MSX {
|
||||
struct Target: public ::Analyser::Static::Target {
|
||||
bool has_disk_drive = false;
|
||||
std::string loading_command;
|
||||
|
||||
enum class Region {
|
||||
Japan,
|
||||
USA,
|
||||
Europe
|
||||
} region = Region::USA;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
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 */
|
||||
82
Analyser/Static/Sega/StaticAnalyser.cpp
Normal file
82
Analyser/Static/Sega/StaticAnalyser.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// StaticAnalyser.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/09/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "StaticAnalyser.hpp"
|
||||
|
||||
#include "Target.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::Sega::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
|
||||
if(media.cartridges.empty())
|
||||
return {};
|
||||
|
||||
TargetList targets;
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
|
||||
target->machine = Machine::MasterSystem;
|
||||
|
||||
// Files named .sg are treated as for the SG1000; otherwise assume a Master System.
|
||||
if(file_name.size() >= 2 && *(file_name.end() - 2) == 's' && *(file_name.end() - 1) == 'g') {
|
||||
target->model = Target::Model::SG1000;
|
||||
} else {
|
||||
target->model = Target::Model::MasterSystem;
|
||||
}
|
||||
|
||||
// If this is a Master System title, look for a ROM header.
|
||||
if(target->model == Target::Model::MasterSystem) {
|
||||
const auto &data = media.cartridges.front()->get_segments()[0].data;
|
||||
|
||||
// First try to locate a header.
|
||||
size_t header_offset = 0;
|
||||
size_t potential_offsets[] = {0x1ff0, 0x3ff0, 0x7ff0};
|
||||
for(auto potential_offset: potential_offsets) {
|
||||
if(!memcmp(&data[potential_offset], "TMR SEGA", 8)) {
|
||||
header_offset = potential_offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If a header was found, use it to crib region.
|
||||
if(header_offset) {
|
||||
// Treat export titles as European by default; decline to
|
||||
// do so only if (US) or (NTSC) is in the file name.
|
||||
const uint8_t region = data[header_offset + 0x0f] >> 4;
|
||||
switch(region) {
|
||||
default: break;
|
||||
case 4: {
|
||||
std::string lowercase_name = file_name;
|
||||
std::transform(lowercase_name.begin(), lowercase_name.end(), lowercase_name.begin(), ::tolower);
|
||||
if(lowercase_name.find("(jp)") == std::string::npos) {
|
||||
target->region =
|
||||
(lowercase_name.find("(us)") == std::string::npos &&
|
||||
lowercase_name.find("(ntsc)") == std::string::npos) ? Target::Region::Europe : Target::Region::USA;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
// Also check for a Codemasters header.
|
||||
// If one is found, set the paging scheme appropriately.
|
||||
const uint16_t inverse_checksum = uint16_t(0x10000 - (data[0x7fe6] | (data[0x7fe7] << 8)));
|
||||
if(
|
||||
data[0x7fe3] >= 0x87 && data[0x7fe3] < 0x96 && // i.e. game is dated between 1987 and 1996
|
||||
(inverse_checksum&0xff) == data[0x7fe8] &&
|
||||
(inverse_checksum >> 8) == data[0x7fe9] && // i.e. the standard checksum appears to be present
|
||||
!data[0x7fea] && !data[0x7feb] && !data[0x7fec] && !data[0x7fed] && !data[0x7fee] && !data[0x7fef]
|
||||
) {
|
||||
target->paging_scheme = Target::PagingScheme::Codemasters;
|
||||
target->model = Target::Model::MasterSystem2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target->media.cartridges = media.cartridges;
|
||||
targets.push_back(std::move(target));
|
||||
return targets;
|
||||
}
|
||||
26
Analyser/Static/Sega/StaticAnalyser.hpp
Normal file
26
Analyser/Static/Sega/StaticAnalyser.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// StaticAnalyser.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/09/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef StaticAnalyser_Sega_StaticAnalyser_hpp
|
||||
#define StaticAnalyser_Sega_StaticAnalyser_hpp
|
||||
|
||||
#include "../StaticAnalyser.hpp"
|
||||
#include "../../../Storage/TargetPlatforms.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Sega {
|
||||
|
||||
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* StaticAnalyser_hpp */
|
||||
46
Analyser/Static/Sega/Target.hpp
Normal file
46
Analyser/Static/Sega/Target.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// Target.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 23/09/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_Sega_Target_h
|
||||
#define Analyser_Static_Sega_Target_h
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Sega {
|
||||
|
||||
struct Target: public ::Analyser::Static::Target {
|
||||
enum class Model {
|
||||
SG1000,
|
||||
MasterSystem,
|
||||
MasterSystem2,
|
||||
};
|
||||
|
||||
enum class Region {
|
||||
Japan,
|
||||
USA,
|
||||
Europe,
|
||||
Brazil
|
||||
};
|
||||
|
||||
enum class PagingScheme {
|
||||
Sega,
|
||||
Codemasters
|
||||
};
|
||||
|
||||
Model model = Model::MasterSystem;
|
||||
Region region = Region::Japan;
|
||||
PagingScheme paging_scheme = PagingScheme::Sega;
|
||||
};
|
||||
|
||||
#define is_master_system(v) v >= Analyser::Static::Sega::Target::Model::MasterSystem
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Analyser_Static_Sega_Target_h */
|
||||
@@ -21,8 +21,10 @@
|
||||
#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"
|
||||
#include "ZX8081/StaticAnalyser.hpp"
|
||||
|
||||
// Cartridges
|
||||
@@ -34,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"
|
||||
@@ -84,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") {
|
||||
@@ -128,14 +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("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
|
||||
@@ -170,8 +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::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:
|
||||
|
||||
82
ClockReceiver/DeferredQueue.hpp
Normal file
82
ClockReceiver/DeferredQueue.hpp
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// DeferredQueue.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 23/08/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef DeferredQueue_h
|
||||
#define DeferredQueue_h
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
/*!
|
||||
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 DeferredQueue {
|
||||
public:
|
||||
/// 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.
|
||||
|
||||
Actions must be scheduled in the order they will occur. It is undefined behaviour
|
||||
to schedule them out of order.
|
||||
*/
|
||||
void defer(TimeUnit delay, const std::function<void(void)> &action) {
|
||||
pending_actions_.emplace_back(delay, action);
|
||||
}
|
||||
|
||||
/*!
|
||||
Runs for @c length units of time.
|
||||
|
||||
The constructor-supplied target will be called with one or more periods that add up to @c length;
|
||||
any scheduled actions will be called between periods.
|
||||
*/
|
||||
void run_for(TimeUnit length) {
|
||||
// If there are no pending actions, just run for the entire length.
|
||||
// This should be the normal branch.
|
||||
if(pending_actions_.empty()) {
|
||||
target_(length);
|
||||
return;
|
||||
}
|
||||
|
||||
// Divide the time to run according to the pending actions.
|
||||
while(length > TimeUnit(0)) {
|
||||
TimeUnit next_period = pending_actions_.empty() ? length : std::min(length, pending_actions_[0].delay);
|
||||
target_(next_period);
|
||||
length -= next_period;
|
||||
|
||||
off_t performances = 0;
|
||||
for(auto &action: pending_actions_) {
|
||||
action.delay -= next_period;
|
||||
if(!action.delay) {
|
||||
action.action();
|
||||
++performances;
|
||||
}
|
||||
}
|
||||
if(performances) {
|
||||
pending_actions_.erase(pending_actions_.begin(), pending_actions_.begin() + performances);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void(TimeUnit)> target_;
|
||||
|
||||
// The list of deferred actions.
|
||||
struct DeferredAction {
|
||||
TimeUnit delay;
|
||||
std::function<void(void)> action;
|
||||
|
||||
DeferredAction(TimeUnit delay, const std::function<void(void)> &action) : delay(delay), action(std::move(action)) {}
|
||||
};
|
||||
std::vector<DeferredAction> pending_actions_;
|
||||
};
|
||||
|
||||
#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 */
|
||||
@@ -141,22 +141,22 @@ void WD1770::run_for(const Cycles cycles) {
|
||||
WAIT_FOR_EVENT(Event1770::IndexHoleTarget); \
|
||||
status_.spin_up = true;
|
||||
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 1 ! Restore ! 0 0 0 0 h v r1 r0 !
|
||||
// ! 1 ! Seek ! 0 0 0 1 h v r1 r0 !
|
||||
// ! 1 ! Step ! 0 0 1 u h v r1 r0 !
|
||||
// ! 1 ! Step-in ! 0 1 0 u h v r1 r0 !
|
||||
// ! 1 ! Step-out ! 0 1 1 u h v r1 r0 !
|
||||
// ! 2 ! Rd sectr ! 1 0 0 m h E 0 0 !
|
||||
// ! 2 ! Wt sectr ! 1 0 1 m h E P a0 !
|
||||
// ! 3 ! Rd addr ! 1 1 0 0 h E 0 0 !
|
||||
// ! 3 ! Rd track ! 1 1 1 0 h E 0 0 !
|
||||
// ! 3 ! Wt track ! 1 1 1 1 h E P 0 !
|
||||
// ! 4 ! Forc int ! 1 1 0 1 i3 i2 i1 i0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 1 ! Restore ! 0 0 0 0 h v r1 r0 !
|
||||
// ! 1 ! Seek ! 0 0 0 1 h v r1 r0 !
|
||||
// ! 1 ! Step ! 0 0 1 u h v r1 r0 !
|
||||
// ! 1 ! Step-in ! 0 1 0 u h v r1 r0 !
|
||||
// ! 1 ! Step-out ! 0 1 1 u h v r1 r0 !
|
||||
// ! 2 ! Rd sectr ! 1 0 0 m h E 0 0 !
|
||||
// ! 2 ! Wt sectr ! 1 0 1 m h E P a0 !
|
||||
// ! 3 ! Rd addr ! 1 1 0 0 h E 0 0 !
|
||||
// ! 3 ! Rd track ! 1 1 1 0 h E 0 0 !
|
||||
// ! 3 ! Wt track ! 1 1 1 1 h E P 0 !
|
||||
// ! 4 ! Forc int ! 1 1 0 1 i3 i2 i1 i0 !
|
||||
// +--------+----------+-------------------------+
|
||||
|
||||
void WD1770::posit_event(int new_event_type) {
|
||||
if(new_event_type == static_cast<int>(Event::IndexHole)) {
|
||||
@@ -189,7 +189,6 @@ void WD1770::posit_event(int new_event_type) {
|
||||
interesting_event_mask_ &= ~new_event_type;
|
||||
}
|
||||
|
||||
Status new_status;
|
||||
BEGIN_SECTION()
|
||||
|
||||
// Wait for a new command, branch to the appropriate handler.
|
||||
@@ -211,7 +210,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
status.interrupt_request = false;
|
||||
});
|
||||
|
||||
LOG("Starting " << std::hex << command_ << std::endl);
|
||||
LOG("Starting " << PADHEX(2) << int(command_));
|
||||
|
||||
if(!(command_ & 0x80)) goto begin_type_1;
|
||||
if(!(command_ & 0x40)) goto begin_type_2;
|
||||
@@ -221,16 +220,16 @@ void WD1770::posit_event(int new_event_type) {
|
||||
/*
|
||||
Type 1 entry point.
|
||||
*/
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 1 ! Restore ! 0 0 0 0 h v r1 r0 !
|
||||
// ! 1 ! Seek ! 0 0 0 1 h v r1 r0 !
|
||||
// ! 1 ! Step ! 0 0 1 u h v r1 r0 !
|
||||
// ! 1 ! Step-in ! 0 1 0 u h v r1 r0 !
|
||||
// ! 1 ! Step-out ! 0 1 1 u h v r1 r0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 1 ! Restore ! 0 0 0 0 h v r1 r0 !
|
||||
// ! 1 ! Seek ! 0 0 0 1 h v r1 r0 !
|
||||
// ! 1 ! Step ! 0 0 1 u h v r1 r0 !
|
||||
// ! 1 ! Step-in ! 0 1 0 u h v r1 r0 !
|
||||
// ! 1 ! Step-out ! 0 1 1 u h v r1 r0 !
|
||||
// +--------+----------+-------------------------+
|
||||
|
||||
begin_type_1:
|
||||
// Set initial flags, skip spin-up if possible.
|
||||
@@ -329,7 +328,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
}
|
||||
|
||||
if(header_[0] == track_) {
|
||||
LOG("Reached track " << std::dec << track_);
|
||||
LOG("Reached track " << std::dec << int(track_));
|
||||
update_status([] (Status &status) {
|
||||
status.crc_error = false;
|
||||
});
|
||||
@@ -344,13 +343,13 @@ void WD1770::posit_event(int new_event_type) {
|
||||
/*
|
||||
Type 2 entry point.
|
||||
*/
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 2 ! Rd sectr ! 1 0 0 m h E 0 0 !
|
||||
// ! 2 ! Wt sectr ! 1 0 1 m h E P a0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 2 ! Rd sectr ! 1 0 0 m h E 0 0 !
|
||||
// ! 2 ! Wt sectr ! 1 0 1 m h E P a0 !
|
||||
// +--------+----------+-------------------------+
|
||||
|
||||
begin_type_2:
|
||||
update_status([] (Status &status) {
|
||||
@@ -398,18 +397,18 @@ void WD1770::posit_event(int new_event_type) {
|
||||
READ_ID();
|
||||
|
||||
if(index_hole_count_ == 5) {
|
||||
LOG("Failed to find sector " << std::dec << sector_);
|
||||
LOG("Failed to find sector " << std::dec << int(sector_));
|
||||
update_status([] (Status &status) {
|
||||
status.record_not_found = true;
|
||||
});
|
||||
goto wait_for_command;
|
||||
}
|
||||
if(distance_into_section_ == 7) {
|
||||
LOG("Considering " << std::dec << header_[0] << "/" << header_[2]);
|
||||
LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
||||
set_data_mode(DataMode::Scanning);
|
||||
if( header_[0] == track_ && header_[2] == sector_ &&
|
||||
(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) {
|
||||
LOG("Found " << std::dec << header_[0] << "/" << header_[2]);
|
||||
LOG("Found " << std::dec << int(header_[0]) << "/" << int(header_[2]));
|
||||
if(get_crc_generator().get_value()) {
|
||||
LOG("CRC error; back to searching");
|
||||
update_status([] (Status &status) {
|
||||
@@ -478,7 +477,7 @@ void WD1770::posit_event(int new_event_type) {
|
||||
sector_++;
|
||||
goto test_type2_write_protection;
|
||||
}
|
||||
LOG("Finished reading sector " << std::dec << sector_);
|
||||
LOG("Finished reading sector " << std::dec << int(sector_));
|
||||
goto wait_for_command;
|
||||
}
|
||||
goto type2_check_crc;
|
||||
@@ -560,21 +559,21 @@ void WD1770::posit_event(int new_event_type) {
|
||||
sector_++;
|
||||
goto test_type2_write_protection;
|
||||
}
|
||||
LOG("Wrote sector " << std::dec << sector_);
|
||||
LOG("Wrote sector " << std::dec << int(sector_));
|
||||
goto wait_for_command;
|
||||
|
||||
|
||||
/*
|
||||
Type 3 entry point.
|
||||
*/
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 3 ! Rd addr ! 1 1 0 0 h E 0 0 !
|
||||
// ! 3 ! Rd track ! 1 1 1 0 h E 0 0 !
|
||||
// ! 3 ! Wt track ! 1 1 1 1 h E P 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// +--------+----------+-------------------------+
|
||||
// ! ! ! BITS !
|
||||
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
// ! 3 ! Rd addr ! 1 1 0 0 h E 0 0 !
|
||||
// ! 3 ! Rd track ! 1 1 1 0 h E 0 0 !
|
||||
// ! 3 ! Wt track ! 1 1 1 1 h E P 0 !
|
||||
// +--------+----------+-------------------------+
|
||||
begin_type_3:
|
||||
update_status([] (Status &status) {
|
||||
status.type = Status::Three;
|
||||
|
||||
@@ -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,16 +102,44 @@ 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();
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#include "Implementation/6522Implementation.hpp"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* _522_hpp */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -6,29 +6,63 @@
|
||||
// Copyright 2017 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) {
|
||||
address &= 0xf;
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
namespace MOS {
|
||||
namespace MOS6522 {
|
||||
|
||||
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;
|
||||
|
||||
@@ -54,32 +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:
|
||||
// printf("Peripheral control %02x\n", value);
|
||||
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: printf("Unimplemented control line mode %d\n", (value >> 1)&7); break;
|
||||
case 0x0c: bus_handler_.set_control_line_output(Port::A, Line::Two, false); break;
|
||||
case 0x0e: bus_handler_.set_control_line_output(Port::A, Line::Two, true); break;
|
||||
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: printf("Unimplemented control line mode %d\n", (value >> 5)&7); break;
|
||||
case 0xc0: bus_handler_.set_control_line_output(Port::B, Line::Two, false); break;
|
||||
case 0xe0: bus_handler_.set_control_line_output(Port::B, Line::Two, true); break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
} break;
|
||||
|
||||
// Interrupt control
|
||||
case 0xd:
|
||||
@@ -98,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();
|
||||
@@ -128,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;
|
||||
@@ -141,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);
|
||||
}
|
||||
|
||||
@@ -154,6 +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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
||||
unsigned int counters_[4] = {2, 1, 0, 0}; // create a slight phase offset for the three channels
|
||||
unsigned int counters_[4] = {2, 1, 0, 0}; // create a slight phase offset for the three channels
|
||||
unsigned int shift_registers_[4] = {0, 0, 0, 0};
|
||||
uint8_t control_registers_[4] = {0, 0, 0, 0};
|
||||
int16_t volume_ = 0;
|
||||
@@ -64,23 +64,12 @@ template <class BusHandler> class MOS6560 {
|
||||
public:
|
||||
MOS6560(BusHandler &bus_handler) :
|
||||
bus_handler_(bus_handler),
|
||||
crt_(new Outputs::CRT::CRT(65*4, 4, Outputs::CRT::DisplayType::NTSC60, 2)),
|
||||
crt_(65*4, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8),
|
||||
audio_generator_(audio_queue_),
|
||||
speaker_(audio_generator_)
|
||||
{
|
||||
crt_->set_svideo_sampling_function(
|
||||
"vec2 svideo_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
|
||||
"{"
|
||||
"vec2 yc = texture(texID, coordinate).rg / vec2(255.0);"
|
||||
|
||||
"float phaseOffset = 6.283185308 * 2.0 * yc.y;"
|
||||
"float chroma = step(yc.y, 0.75) * cos(phase + phaseOffset);"
|
||||
|
||||
"return vec2(yc.x, chroma);"
|
||||
"}");
|
||||
|
||||
// default to s-video output
|
||||
crt_->set_video_signal(Outputs::CRT::VideoSignal::SVideo);
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::SVideo);
|
||||
|
||||
// default to NTSC
|
||||
set_output_mode(OutputMode::NTSC);
|
||||
@@ -94,7 +83,8 @@ template <class BusHandler> class MOS6560 {
|
||||
speaker_.set_input_rate(static_cast<float>(clock_rate / 4.0));
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *get_crt() { return crt_.get(); }
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); }
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); }
|
||||
Outputs::Speaker::Speaker *get_speaker() { return &speaker_; }
|
||||
|
||||
void set_high_frequency_cutoff(float cutoff) {
|
||||
@@ -117,12 +107,12 @@ template <class BusHandler> class MOS6560 {
|
||||
|
||||
// Chrominances are encoded such that 0-128 is a complete revolution of phase;
|
||||
// anything above 191 disables the colour subcarrier. Phase is relative to the
|
||||
// colour burst, so 0 is green.
|
||||
// colour burst, so 0 is green (NTSC) or blue/violet (PAL).
|
||||
const uint8_t pal_chrominances[16] = {
|
||||
255, 255, 37, 101,
|
||||
19, 86, 123, 59,
|
||||
46, 53, 37, 101,
|
||||
19, 86, 123, 59,
|
||||
255, 255, 90, 20,
|
||||
96, 42, 8, 72,
|
||||
84, 90, 90, 20,
|
||||
96, 42, 8, 72,
|
||||
};
|
||||
const uint8_t ntsc_chrominances[16] = {
|
||||
255, 255, 121, 57,
|
||||
@@ -131,12 +121,12 @@ template <class BusHandler> class MOS6560 {
|
||||
103, 42, 80, 16,
|
||||
};
|
||||
const uint8_t *chrominances;
|
||||
Outputs::CRT::DisplayType display_type;
|
||||
Outputs::Display::Type display_type;
|
||||
|
||||
switch(output_mode) {
|
||||
default:
|
||||
chrominances = pal_chrominances;
|
||||
display_type = Outputs::CRT::DisplayType::PAL50;
|
||||
display_type = Outputs::Display::Type::PAL50;
|
||||
timing_.cycles_per_line = 71;
|
||||
timing_.line_counter_increment_offset = 4;
|
||||
timing_.final_line_increment_position = timing_.cycles_per_line - timing_.line_counter_increment_offset;
|
||||
@@ -146,7 +136,7 @@ template <class BusHandler> class MOS6560 {
|
||||
|
||||
case OutputMode::NTSC:
|
||||
chrominances = ntsc_chrominances;
|
||||
display_type = Outputs::CRT::DisplayType::NTSC60;
|
||||
display_type = Outputs::Display::Type::NTSC60;
|
||||
timing_.cycles_per_line = 65;
|
||||
timing_.line_counter_increment_offset = 40;
|
||||
timing_.final_line_increment_position = 58;
|
||||
@@ -155,14 +145,14 @@ template <class BusHandler> class MOS6560 {
|
||||
break;
|
||||
}
|
||||
|
||||
crt_->set_new_display_type(static_cast<unsigned int>(timing_.cycles_per_line*4), display_type);
|
||||
crt_.set_new_display_type(timing_.cycles_per_line*4, display_type);
|
||||
|
||||
switch(output_mode) {
|
||||
case OutputMode::PAL:
|
||||
crt_->set_visible_area(Outputs::CRT::Rect(0.1f, 0.07f, 0.9f, 0.9f));
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.07f, 0.9f, 0.9f));
|
||||
break;
|
||||
case OutputMode::NTSC:
|
||||
crt_->set_visible_area(Outputs::CRT::Rect(0.05f, 0.05f, 0.9f, 0.9f));
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -284,17 +274,17 @@ template <class BusHandler> class MOS6560 {
|
||||
// update the CRT
|
||||
if(this_state_ != output_state_) {
|
||||
switch(output_state_) {
|
||||
case State::Sync: crt_->output_sync(cycles_in_state_ * 4); break;
|
||||
case State::ColourBurst: crt_->output_colour_burst(cycles_in_state_ * 4, (is_odd_frame_ || is_odd_line_) ? 128 : 0); break;
|
||||
case State::Border: output_border(cycles_in_state_ * 4); break;
|
||||
case State::Pixels: crt_->output_data(cycles_in_state_ * 4); break;
|
||||
case State::Sync: crt_.output_sync(cycles_in_state_ * 4); break;
|
||||
case State::ColourBurst: crt_.output_colour_burst(cycles_in_state_ * 4, (is_odd_frame_ || is_odd_line_) ? 128 : 0); break;
|
||||
case State::Border: output_border(cycles_in_state_ * 4); break;
|
||||
case State::Pixels: crt_.output_data(cycles_in_state_ * 4); break;
|
||||
}
|
||||
output_state_ = this_state_;
|
||||
cycles_in_state_ = 0;
|
||||
|
||||
pixel_pointer = nullptr;
|
||||
if(output_state_ == State::Pixels) {
|
||||
pixel_pointer = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(260));
|
||||
pixel_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(260));
|
||||
}
|
||||
}
|
||||
cycles_in_state_++;
|
||||
@@ -438,7 +428,7 @@ template <class BusHandler> class MOS6560 {
|
||||
|
||||
private:
|
||||
BusHandler &bus_handler_;
|
||||
std::unique_ptr<Outputs::CRT::CRT> crt_;
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
AudioGenerator audio_generator_;
|
||||
@@ -451,12 +441,12 @@ template <class BusHandler> class MOS6560 {
|
||||
|
||||
// register state
|
||||
struct {
|
||||
bool interlaced, tall_characters;
|
||||
bool interlaced = false, tall_characters = false;
|
||||
uint8_t first_column_location, first_row_location;
|
||||
uint8_t number_of_columns, number_of_rows;
|
||||
uint16_t character_cell_start_address, video_matrix_start_address;
|
||||
uint16_t backgroundColour, borderColour, auxiliary_colour;
|
||||
bool invertedCells;
|
||||
bool invertedCells = false;
|
||||
|
||||
uint8_t direct_values[16];
|
||||
} registers_;
|
||||
@@ -465,7 +455,7 @@ template <class BusHandler> class MOS6560 {
|
||||
enum State {
|
||||
Sync, ColourBurst, Border, Pixels
|
||||
} this_state_, output_state_;
|
||||
unsigned int cycles_in_state_;
|
||||
int cycles_in_state_;
|
||||
|
||||
// counters that cover an entire field
|
||||
int horizontal_counter_ = 0, vertical_counter_ = 0;
|
||||
@@ -493,7 +483,7 @@ template <class BusHandler> class MOS6560 {
|
||||
}
|
||||
|
||||
// latches dictating start and length of drawing
|
||||
bool vertical_drawing_latch_, horizontal_drawing_latch_;
|
||||
bool vertical_drawing_latch_ = false, horizontal_drawing_latch_ = false;
|
||||
int rows_this_field_, columns_this_line_;
|
||||
|
||||
// current drawing position counter
|
||||
@@ -511,10 +501,10 @@ template <class BusHandler> class MOS6560 {
|
||||
uint16_t colours_[16];
|
||||
|
||||
uint16_t *pixel_pointer;
|
||||
void output_border(unsigned int number_of_cycles) {
|
||||
uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(1));
|
||||
void output_border(int number_of_cycles) {
|
||||
uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1));
|
||||
if(colour_pointer) *colour_pointer = registers_.borderColour;
|
||||
crt_->output_level(number_of_cycles);
|
||||
crt_.output_level(number_of_cycles);
|
||||
}
|
||||
|
||||
struct {
|
||||
|
||||
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 */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
||||
#include <cstdint>
|
||||
|
||||
namespace TI {
|
||||
namespace TMS {
|
||||
|
||||
/*!
|
||||
Provides emulation of the TMS9918a, TMS9928 and TMS9929. Likely in the future to be the
|
||||
@@ -29,30 +30,22 @@ namespace TI {
|
||||
These chips have only one non-on-demand interaction with the outside world: an interrupt line.
|
||||
See get_time_until_interrupt and get_interrupt_line for asynchronous operation options.
|
||||
*/
|
||||
class TMS9918: public TMS9918Base {
|
||||
class TMS9918: public Base {
|
||||
public:
|
||||
enum Personality {
|
||||
TMS9918A, // includes the 9928 and 9929; set TV standard and output device as desired.
|
||||
};
|
||||
|
||||
/*!
|
||||
Constructs an instance of the drive controller that behaves according to personality @c p.
|
||||
@param p The type of controller to emulate.
|
||||
*/
|
||||
TMS9918(Personality p);
|
||||
|
||||
enum TVStandard {
|
||||
/*! i.e. 50Hz output at around 312.5 lines/field */
|
||||
PAL,
|
||||
/*! i.e. 60Hz output at around 262.5 lines/field */
|
||||
NTSC
|
||||
};
|
||||
|
||||
/*! Sets the TV standard for this TMS, if that is hard-coded in hardware. */
|
||||
void set_tv_standard(TVStandard standard);
|
||||
|
||||
/*! Provides the CRT this TMS is connected to. */
|
||||
Outputs::CRT::CRT *get_crt();
|
||||
/*! Sets the scan target this TMS will post content to. */
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
|
||||
/*! Sets the type of display the CRT will request. */
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
/*!
|
||||
Runs the VCP for the number of cycles indicate; it is an implicit assumption of the code
|
||||
@@ -66,6 +59,15 @@ class TMS9918: public TMS9918Base {
|
||||
/*! Gets a register value. */
|
||||
uint8_t get_register(int address);
|
||||
|
||||
/*! Gets the current scan line; provided by the Master System only. */
|
||||
uint8_t get_current_line();
|
||||
|
||||
/*! Gets the current latched horizontal counter; provided by the Master System only. */
|
||||
uint8_t get_latched_horizontal_counter();
|
||||
|
||||
/*! Latches the current horizontal counter. */
|
||||
void latch_horizontal_counter();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until get_interrupt_line would next return true if
|
||||
there are no interceding calls to set_register or get_register.
|
||||
@@ -75,12 +77,23 @@ class TMS9918: public TMS9918Base {
|
||||
*/
|
||||
HalfCycles get_time_until_interrupt();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until the nominated line interrupt position is
|
||||
reached on line @c line. If no line interrupt position is defined for
|
||||
this VDP, returns the time until the 'beginning' of that line, whatever
|
||||
that may mean.
|
||||
|
||||
@line is relative to the first pixel line of the display and may be negative.
|
||||
*/
|
||||
HalfCycles get_time_until_line(int line);
|
||||
|
||||
/*!
|
||||
@returns @c true if the interrupt line is currently active; @c false otherwise.
|
||||
*/
|
||||
bool get_interrupt_line();
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* TMS9918_hpp */
|
||||
|
||||
@@ -12,90 +12,848 @@
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace TI {
|
||||
namespace TMS {
|
||||
|
||||
enum Personality {
|
||||
TMS9918A, // includes the 9928 and 9929; set TV standard and output device as desired.
|
||||
V9938,
|
||||
V9958,
|
||||
SMSVDP,
|
||||
SMS2VDP,
|
||||
GGVDP,
|
||||
};
|
||||
|
||||
enum class TVStandard {
|
||||
/*! i.e. 50Hz output at around 312.5 lines/field */
|
||||
PAL,
|
||||
/*! i.e. 60Hz output at around 262.5 lines/field */
|
||||
NTSC
|
||||
};
|
||||
|
||||
#define is_sega_vdp(x) ((x) >= SMSVDP)
|
||||
|
||||
class Base {
|
||||
public:
|
||||
static const uint32_t palette_pack(uint8_t r, uint8_t g, uint8_t b) {
|
||||
uint32_t result = 0;
|
||||
uint8_t *const result_ptr = reinterpret_cast<uint8_t *>(&result);
|
||||
result_ptr[0] = r;
|
||||
result_ptr[1] = g;
|
||||
result_ptr[2] = b;
|
||||
result_ptr[3] = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
class TMS9918Base {
|
||||
protected:
|
||||
TMS9918Base();
|
||||
const static int output_lag = 11; // i.e. pixel output will occur 11 cycles after corresponding data read.
|
||||
|
||||
std::unique_ptr<Outputs::CRT::CRT> crt_;
|
||||
// The default TMS palette.
|
||||
const uint32_t palette[16] = {
|
||||
palette_pack(0, 0, 0),
|
||||
palette_pack(0, 0, 0),
|
||||
palette_pack(33, 200, 66),
|
||||
palette_pack(94, 220, 120),
|
||||
|
||||
uint8_t ram_[16384];
|
||||
palette_pack(84, 85, 237),
|
||||
palette_pack(125, 118, 252),
|
||||
palette_pack(212, 82, 77),
|
||||
palette_pack(66, 235, 245),
|
||||
|
||||
palette_pack(252, 85, 84),
|
||||
palette_pack(255, 121, 120),
|
||||
palette_pack(212, 193, 84),
|
||||
palette_pack(230, 206, 128),
|
||||
|
||||
palette_pack(33, 176, 59),
|
||||
palette_pack(201, 91, 186),
|
||||
palette_pack(204, 204, 204),
|
||||
palette_pack(255, 255, 255)
|
||||
};
|
||||
|
||||
Base(Personality p);
|
||||
|
||||
const Personality personality_;
|
||||
Outputs::CRT::CRT crt_;
|
||||
TVStandard tv_standard_ = TVStandard::NTSC;
|
||||
|
||||
// Holds the contents of this VDP's connected DRAM.
|
||||
std::vector<uint8_t> ram_;
|
||||
|
||||
// Holds the state of the DRAM/CRAM-access mechanism.
|
||||
uint16_t ram_pointer_ = 0;
|
||||
uint8_t read_ahead_buffer_ = 0;
|
||||
enum class MemoryAccess {
|
||||
Read, Write, None
|
||||
} queued_access_ = MemoryAccess::None;
|
||||
int cycles_until_access_ = 0;
|
||||
int minimum_access_column_ = 0;
|
||||
int vram_access_delay() {
|
||||
// This seems to be correct for all currently-modelled VDPs;
|
||||
// it's the delay between an external device scheduling a
|
||||
// read or write and the very first time that can occur
|
||||
// (though, in practice, it won't happen until the next
|
||||
// external slot after this number of cycles after the
|
||||
// device has requested the read or write).
|
||||
return 7;
|
||||
}
|
||||
|
||||
// Holds the main status register.
|
||||
uint8_t status_ = 0;
|
||||
|
||||
bool write_phase_ = false;
|
||||
uint8_t low_write_ = 0;
|
||||
// Current state of programmer input.
|
||||
bool write_phase_ = false; // Determines whether the VDP is expecting the low or high byte of a write.
|
||||
uint8_t low_write_ = 0; // Buffers the low byte of a write.
|
||||
|
||||
// The various register flags.
|
||||
int next_screen_mode_ = 0, screen_mode_ = 0;
|
||||
bool next_blank_screen_ = true, blank_screen_ = true;
|
||||
// Various programmable flags.
|
||||
bool mode1_enable_ = false;
|
||||
bool mode2_enable_ = false;
|
||||
bool mode3_enable_ = false;
|
||||
bool blank_display_ = false;
|
||||
bool sprites_16x16_ = false;
|
||||
bool sprites_magnified_ = false;
|
||||
bool generate_interrupts_ = false;
|
||||
int sprite_height_ = 8;
|
||||
uint16_t pattern_name_address_ = 0;
|
||||
uint16_t colour_table_address_ = 0;
|
||||
uint16_t pattern_generator_table_address_ = 0;
|
||||
uint16_t sprite_attribute_table_address_ = 0;
|
||||
uint16_t sprite_generator_table_address_ = 0;
|
||||
|
||||
size_t pattern_name_address_ = 0; // i.e. address of the tile map.
|
||||
size_t colour_table_address_ = 0; // address of the colour map (if applicable).
|
||||
size_t pattern_generator_table_address_ = 0; // address of the tile contents.
|
||||
size_t sprite_attribute_table_address_ = 0; // address of the sprite list.
|
||||
size_t sprite_generator_table_address_ = 0; // address of the sprite contents.
|
||||
|
||||
uint8_t text_colour_ = 0;
|
||||
uint8_t background_colour_ = 0;
|
||||
|
||||
HalfCycles half_cycles_into_frame_;
|
||||
int column_ = 0, row_ = 0, output_column_ = 0;
|
||||
// This implementation of this chip officially accepts a 3.58Mhz clock, but runs
|
||||
// internally at 5.37Mhz. The following two help to maintain a lossless conversion
|
||||
// from the one to the other.
|
||||
int cycles_error_ = 0;
|
||||
uint32_t *pixel_target_ = nullptr, *pixel_base_ = nullptr;
|
||||
HalfCycles half_cycles_before_internal_cycles(int internal_cycles);
|
||||
|
||||
void output_border(int cycles);
|
||||
// Internal mechanisms for position tracking.
|
||||
int latched_column_ = 0;
|
||||
|
||||
// Vertical timing details.
|
||||
int frame_lines_ = 262;
|
||||
int first_vsync_line_ = 227;
|
||||
// A helper function to output the current border colour for
|
||||
// the number of cycles supplied.
|
||||
void output_border(int cycles, uint32_t cram_dot);
|
||||
|
||||
// A struct to contain timing information for the current mode.
|
||||
struct {
|
||||
/*
|
||||
Vertical layout:
|
||||
|
||||
Lines 0 to [pixel_lines]: standard data fetch and drawing will occur.
|
||||
... to [first_vsync_line]: refresh fetches will occur and border will be output.
|
||||
.. to [2.5 or 3 lines later]: vertical sync is output.
|
||||
... to [total lines - 1]: refresh fetches will occur and border will be output.
|
||||
... for one line: standard data fetch will occur, without drawing.
|
||||
*/
|
||||
int total_lines = 262;
|
||||
int pixel_lines = 192;
|
||||
int first_vsync_line = 227;
|
||||
|
||||
// Maximum number of sprite slots to populate;
|
||||
// if sprites beyond this number should be visible
|
||||
// then the appropriate status information will be set.
|
||||
int maximum_visible_sprites = 4;
|
||||
|
||||
// Set the position, in cycles, of the two interrupts,
|
||||
// within a line.
|
||||
struct {
|
||||
int column = 4;
|
||||
int row = 193;
|
||||
} end_of_frame_interrupt_position;
|
||||
int line_interrupt_position = -1;
|
||||
|
||||
// Enables or disabled the recognition of the sprite
|
||||
// list terminator, and sets the terminator value.
|
||||
bool allow_sprite_terminator = true;
|
||||
uint8_t sprite_terminator = 0xd0;
|
||||
} mode_timing_;
|
||||
|
||||
uint8_t line_interrupt_target = 0xff;
|
||||
uint8_t line_interrupt_counter = 0;
|
||||
bool enable_line_interrupts_ = false;
|
||||
bool line_interrupt_pending_ = false;
|
||||
|
||||
// The screen mode is a necessary predecessor to picking the line mode,
|
||||
// which is the thing latched per line.
|
||||
enum class ScreenMode {
|
||||
Blank,
|
||||
Text,
|
||||
MultiColour,
|
||||
ColouredText,
|
||||
Graphics,
|
||||
SMSMode4
|
||||
} screen_mode_;
|
||||
|
||||
// Horizontal selections.
|
||||
enum class LineMode {
|
||||
Text = 0,
|
||||
Character = 1,
|
||||
Refresh = 2
|
||||
} line_mode_ = LineMode::Text;
|
||||
int first_pixel_column_, first_right_border_column_;
|
||||
Text,
|
||||
Character,
|
||||
Refresh,
|
||||
SMS
|
||||
};
|
||||
|
||||
uint8_t pattern_names_[40];
|
||||
uint8_t pattern_buffer_[40];
|
||||
uint8_t colour_buffer_[40];
|
||||
// Temporary buffers collect a representation of this line prior to pixel serialisation.
|
||||
struct LineBuffer {
|
||||
// The line mode describes the proper timing diagram for this line.
|
||||
LineMode line_mode = LineMode::Text;
|
||||
|
||||
struct SpriteSet {
|
||||
// Holds the horizontal scroll position to apply to this line;
|
||||
// of those VDPs currently implemented, affects the Master System only.
|
||||
uint8_t latched_horizontal_scroll = 0;
|
||||
|
||||
// The names array holds pattern names, as an offset into memory, and
|
||||
// potentially flags also.
|
||||
struct {
|
||||
size_t offset = 0;
|
||||
uint8_t flags = 0;
|
||||
} names[40];
|
||||
|
||||
// The patterns array holds tile patterns, corresponding 1:1 with names.
|
||||
// Four bytes per pattern is the maximum required by any
|
||||
// currently-implemented VDP.
|
||||
uint8_t patterns[40][4];
|
||||
|
||||
/*
|
||||
Horizontal layout (on a 342-cycle clock):
|
||||
|
||||
15 cycles right border
|
||||
58 cycles blanking & sync
|
||||
13 cycles left border
|
||||
|
||||
... i.e. to cycle 86, then:
|
||||
|
||||
border up to first_pixel_output_column;
|
||||
pixels up to next_border_column;
|
||||
border up to the end.
|
||||
|
||||
e.g. standard 256-pixel modes will want to set
|
||||
first_pixel_output_column = 86, next_border_column = 342.
|
||||
*/
|
||||
int first_pixel_output_column = 94;
|
||||
int next_border_column = 334;
|
||||
|
||||
// An active sprite is one that has been selected for composition onto
|
||||
// this line.
|
||||
struct ActiveSprite {
|
||||
int index = 0;
|
||||
int row = 0;
|
||||
int index = 0; // The original in-table index of this sprite.
|
||||
int row = 0; // The row of the sprite that should be drawn.
|
||||
int x = 0; // The sprite's x position on screen.
|
||||
|
||||
uint8_t info[4];
|
||||
uint8_t image[2];
|
||||
uint8_t image[4]; // Up to four bytes of image information.
|
||||
int shift_position = 0; // An offset representing how much of the image information has already been drawn.
|
||||
} active_sprites[8];
|
||||
|
||||
int shift_position = 0;
|
||||
} active_sprites[4];
|
||||
int active_sprite_slot = 0;
|
||||
} sprite_sets_[2];
|
||||
int active_sprite_set_ = 0;
|
||||
bool sprites_stopped_ = false;
|
||||
int active_sprite_slot = 0; // A pointer to the slot into which a new active sprite will be deposited, if required.
|
||||
bool sprites_stopped = false; // A special TMS feature is that a sentinel value can be used to prevent any further sprites
|
||||
// being evaluated for display. This flag determines whether the sentinel has yet been reached.
|
||||
|
||||
int access_pointer_ = 0;
|
||||
void reset_sprite_collection();
|
||||
} line_buffers_[313];
|
||||
void posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_y, int screen_row);
|
||||
|
||||
inline void test_sprite(int sprite_number, int screen_row);
|
||||
inline void get_sprite_contents(int start, int cycles, int screen_row);
|
||||
// There is a delay between reading into the line buffer and outputting from there to the screen. That delay
|
||||
// is observeable because reading time affects availability of memory accesses and therefore time in which
|
||||
// to update sprites and tiles, but writing time affects when the palette is used and when the collision flag
|
||||
// may end up being set. So the two processes are slightly decoupled. The end of reading one line may overlap
|
||||
// with the beginning of writing the next, hence the two separate line buffers.
|
||||
struct LineBufferPointer {
|
||||
int row, column;
|
||||
} read_pointer_, write_pointer_;
|
||||
|
||||
// The SMS VDP has a programmer-set colour palette, with a dedicated patch of RAM. But the RAM is only exactly
|
||||
// fast enough for the pixel clock. So when the programmer writes to it, that causes a one-pixel glitch; there
|
||||
// isn't the bandwidth for the read both write to occur simultaneously. The following buffer therefore keeps
|
||||
// track of pending collisions, for visual reproduction.
|
||||
struct CRAMDot {
|
||||
LineBufferPointer location;
|
||||
uint32_t value;
|
||||
};
|
||||
std::vector<CRAMDot> upcoming_cram_dots_;
|
||||
|
||||
// Extra information that affects the Master System output mode.
|
||||
struct {
|
||||
// Programmer-set flags.
|
||||
bool vertical_scroll_lock = false;
|
||||
bool horizontal_scroll_lock = false;
|
||||
bool hide_left_column = false;
|
||||
bool shift_sprites_8px_left = false;
|
||||
bool mode4_enable = false;
|
||||
uint8_t horizontal_scroll = 0;
|
||||
uint8_t vertical_scroll = 0;
|
||||
|
||||
// The Master System's additional colour RAM.
|
||||
uint32_t colour_ram[32];
|
||||
bool cram_is_selected = false;
|
||||
|
||||
// Holds the vertical scroll position for this frame; this is latched
|
||||
// once and cannot dynamically be changed until the next frame.
|
||||
uint8_t latched_vertical_scroll = 0;
|
||||
|
||||
size_t pattern_name_address;
|
||||
size_t sprite_attribute_table_address;
|
||||
size_t sprite_generator_table_address;
|
||||
} master_system_;
|
||||
|
||||
void set_current_screen_mode() {
|
||||
if(blank_display_) {
|
||||
screen_mode_ = ScreenMode::Blank;
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_sega_vdp(personality_) && master_system_.mode4_enable) {
|
||||
screen_mode_ = ScreenMode::SMSMode4;
|
||||
mode_timing_.maximum_visible_sprites = 8;
|
||||
return;
|
||||
}
|
||||
|
||||
mode_timing_.maximum_visible_sprites = 4;
|
||||
if(!mode1_enable_ && !mode2_enable_ && !mode3_enable_) {
|
||||
screen_mode_ = ScreenMode::ColouredText;
|
||||
return;
|
||||
}
|
||||
|
||||
if(mode1_enable_ && !mode2_enable_ && !mode3_enable_) {
|
||||
screen_mode_ = ScreenMode::Text;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!mode1_enable_ && mode2_enable_ && !mode3_enable_) {
|
||||
screen_mode_ = ScreenMode::Graphics;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!mode1_enable_ && !mode2_enable_ && mode3_enable_) {
|
||||
screen_mode_ = ScreenMode::MultiColour;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: undocumented TMS modes.
|
||||
screen_mode_ = ScreenMode::Blank;
|
||||
}
|
||||
|
||||
void do_external_slot(int access_column) {
|
||||
// Don't do anything if the required time for the access to become executable
|
||||
// has yet to pass.
|
||||
if(access_column < minimum_access_column_) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(queued_access_) {
|
||||
default: return;
|
||||
|
||||
case MemoryAccess::Write:
|
||||
if(master_system_.cram_is_selected) {
|
||||
// Adjust the palette.
|
||||
master_system_.colour_ram[ram_pointer_ & 0x1f] = palette_pack(
|
||||
static_cast<uint8_t>(((read_ahead_buffer_ >> 0) & 3) * 255 / 3),
|
||||
static_cast<uint8_t>(((read_ahead_buffer_ >> 2) & 3) * 255 / 3),
|
||||
static_cast<uint8_t>(((read_ahead_buffer_ >> 4) & 3) * 255 / 3)
|
||||
);
|
||||
|
||||
// Schedule a CRAM dot; this is scheduled for wherever it should appear
|
||||
// on screen. So it's wherever the output stream would be now. Which
|
||||
// is output_lag cycles ago from the point of view of the input stream.
|
||||
upcoming_cram_dots_.emplace_back();
|
||||
CRAMDot &dot = upcoming_cram_dots_.back();
|
||||
|
||||
dot.location.column = write_pointer_.column - output_lag;
|
||||
dot.location.row = write_pointer_.row;
|
||||
|
||||
// Handle before this row conditionally; then handle after (or, more realistically,
|
||||
// exactly at the end of) naturally.
|
||||
if(dot.location.column < 0) {
|
||||
--dot.location.row;
|
||||
dot.location.column += 342;
|
||||
}
|
||||
dot.location.row += dot.location.column / 342;
|
||||
dot.location.column %= 342;
|
||||
|
||||
dot.value = master_system_.colour_ram[ram_pointer_ & 0x1f];
|
||||
} else {
|
||||
ram_[ram_pointer_ & 16383] = read_ahead_buffer_;
|
||||
}
|
||||
break;
|
||||
case MemoryAccess::Read:
|
||||
read_ahead_buffer_ = ram_[ram_pointer_ & 16383];
|
||||
break;
|
||||
}
|
||||
++ram_pointer_;
|
||||
queued_access_ = MemoryAccess::None;
|
||||
}
|
||||
|
||||
/*
|
||||
Fetching routines follow below; they obey the following rules:
|
||||
|
||||
1) input is a start position and an end position; they should perform the proper
|
||||
operations for the period: start <= time < end.
|
||||
2) times are measured relative to a 172-cycles-per-line clock (so: they directly
|
||||
count access windows on the TMS and Master System).
|
||||
3) time 0 is the beginning of the access window immediately after the last pattern/data
|
||||
block fetch that would contribute to this line, in a normal 32-column mode. So:
|
||||
|
||||
* it's cycle 309 on Mattias' TMS diagram;
|
||||
* it's cycle 1238 on his V9938 diagram;
|
||||
* it's after the last background render block in Mask of Destiny's Master System timing diagram.
|
||||
|
||||
That division point was selected, albeit arbitrarily, because it puts all the tile
|
||||
fetches for a single line into the same [0, 171] period.
|
||||
|
||||
4) all of these functions are templated with a `use_end` parameter. That will be true if
|
||||
end is < 172, false otherwise. So functions can use it to eliminate should-exit-not checks,
|
||||
for the more usual path of execution.
|
||||
|
||||
Provided for the benefit of the methods below:
|
||||
|
||||
* the function external_slot(), which will perform any pending VRAM read/write.
|
||||
* the macros slot(n) and external_slot(n) which can be used to schedule those things inside a
|
||||
switch(start)-based implementation.
|
||||
|
||||
All functions should just spool data to intermediary storage. This is because for most VDPs there is
|
||||
a decoupling between fetch pattern and output pattern, and it's neater to keep the same division
|
||||
for the exceptions.
|
||||
*/
|
||||
|
||||
#define slot(n) \
|
||||
if(use_end && end == n) return;\
|
||||
case n
|
||||
|
||||
#define external_slot(n) \
|
||||
slot(n): do_external_slot((n)*2);
|
||||
|
||||
#define external_slots_2(n) \
|
||||
external_slot(n); \
|
||||
external_slot(n+1);
|
||||
|
||||
#define external_slots_4(n) \
|
||||
external_slots_2(n); \
|
||||
external_slots_2(n+2);
|
||||
|
||||
#define external_slots_8(n) \
|
||||
external_slots_4(n); \
|
||||
external_slots_4(n+4);
|
||||
|
||||
#define external_slots_16(n) \
|
||||
external_slots_8(n); \
|
||||
external_slots_8(n+8);
|
||||
|
||||
#define external_slots_32(n) \
|
||||
external_slots_16(n); \
|
||||
external_slots_16(n+16);
|
||||
|
||||
|
||||
/***********************************************
|
||||
TMS9918 Fetching Code
|
||||
************************************************/
|
||||
|
||||
template<bool use_end> void fetch_tms_refresh(int start, int end) {
|
||||
#define refresh(location) \
|
||||
slot(location): \
|
||||
external_slot(location+1);
|
||||
|
||||
#define refreshes_2(location) \
|
||||
refresh(location); \
|
||||
refresh(location+2);
|
||||
|
||||
#define refreshes_4(location) \
|
||||
refreshes_2(location); \
|
||||
refreshes_2(location+4);
|
||||
|
||||
#define refreshes_8(location) \
|
||||
refreshes_4(location); \
|
||||
refreshes_4(location+8);
|
||||
|
||||
switch(start) {
|
||||
default: assert(false);
|
||||
|
||||
/* 44 external slots */
|
||||
external_slots_32(0)
|
||||
external_slots_8(32)
|
||||
external_slots_4(40)
|
||||
|
||||
/* 64 refresh/external slot pairs (= 128 windows) */
|
||||
refreshes_8(44);
|
||||
refreshes_8(60);
|
||||
refreshes_8(76);
|
||||
refreshes_8(92);
|
||||
refreshes_8(108);
|
||||
refreshes_8(124);
|
||||
refreshes_8(140);
|
||||
refreshes_8(156);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#undef refreshes_8
|
||||
#undef refreshes_4
|
||||
#undef refreshes_2
|
||||
#undef refresh
|
||||
}
|
||||
|
||||
template<bool use_end> void fetch_tms_text(int start, int end) {
|
||||
#define fetch_tile_name(location, column) slot(location): line_buffer.names[column].offset = ram_[row_base + column];
|
||||
#define fetch_tile_pattern(location, column) slot(location): line_buffer.patterns[column][0] = ram_[row_offset + size_t(line_buffer.names[column].offset << 3)];
|
||||
|
||||
#define fetch_column(location, column) \
|
||||
fetch_tile_name(location, column); \
|
||||
external_slot(location+1); \
|
||||
fetch_tile_pattern(location+2, column);
|
||||
|
||||
#define fetch_columns_2(location, column) \
|
||||
fetch_column(location, column); \
|
||||
fetch_column(location+3, column+1);
|
||||
|
||||
#define fetch_columns_4(location, column) \
|
||||
fetch_columns_2(location, column); \
|
||||
fetch_columns_2(location+6, column+2);
|
||||
|
||||
#define fetch_columns_8(location, column) \
|
||||
fetch_columns_4(location, column); \
|
||||
fetch_columns_4(location+12, column+4);
|
||||
|
||||
LineBuffer &line_buffer = line_buffers_[write_pointer_.row];
|
||||
const size_t row_base = pattern_name_address_ & (0x3c00 | static_cast<size_t>(write_pointer_.row >> 3) * 40);
|
||||
const size_t row_offset = pattern_generator_table_address_ & (0x3800 | (write_pointer_.row & 7));
|
||||
|
||||
switch(start) {
|
||||
default: assert(false);
|
||||
|
||||
/* 47 external slots (= 47 windows) */
|
||||
external_slots_32(0)
|
||||
external_slots_8(32)
|
||||
external_slots_4(40)
|
||||
external_slots_2(44)
|
||||
external_slot(46)
|
||||
|
||||
/* 40 column fetches (= 120 windows) */
|
||||
fetch_columns_8(47, 0);
|
||||
fetch_columns_8(71, 8);
|
||||
fetch_columns_8(95, 16);
|
||||
fetch_columns_8(119, 24);
|
||||
fetch_columns_8(143, 32);
|
||||
|
||||
/* 5 more external slots */
|
||||
external_slots_4(167);
|
||||
external_slot(171);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#undef fetch_columns_8
|
||||
#undef fetch_columns_4
|
||||
#undef fetch_columns_2
|
||||
#undef fetch_column
|
||||
#undef fetch_tile_pattern
|
||||
#undef fetch_tile_name
|
||||
}
|
||||
|
||||
template<bool use_end> void fetch_tms_character(int start, int end) {
|
||||
#define sprite_fetch_coordinates(location, sprite) \
|
||||
slot(location): \
|
||||
slot(location+1): \
|
||||
line_buffer.active_sprites[sprite].x = \
|
||||
ram_[\
|
||||
sprite_attribute_table_address_ & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 2))\
|
||||
];
|
||||
|
||||
// This implementation doesn't refetch Y; it's unclear to me
|
||||
// whether it's refetched.
|
||||
|
||||
#define sprite_fetch_graphics(location, sprite) \
|
||||
slot(location): \
|
||||
slot(location+1): \
|
||||
slot(location+2): \
|
||||
slot(location+3): {\
|
||||
const uint8_t name = ram_[\
|
||||
sprite_attribute_table_address_ & size_t(0x3f82 | (line_buffer.active_sprites[sprite].index << 2))\
|
||||
] & (sprites_16x16_ ? ~3 : ~0);\
|
||||
line_buffer.active_sprites[sprite].image[2] = ram_[\
|
||||
sprite_attribute_table_address_ & size_t(0x3f83 | (line_buffer.active_sprites[sprite].index << 2))\
|
||||
];\
|
||||
line_buffer.active_sprites[sprite].x -= (line_buffer.active_sprites[sprite].image[2] & 0x80) >> 2;\
|
||||
const size_t graphic_location = sprite_generator_table_address_ & size_t(0x3800 | (name << 3) | line_buffer.active_sprites[sprite].row); \
|
||||
line_buffer.active_sprites[sprite].image[0] = ram_[graphic_location];\
|
||||
line_buffer.active_sprites[sprite].image[1] = ram_[graphic_location+16];\
|
||||
}
|
||||
|
||||
#define sprite_fetch_block(location, sprite) \
|
||||
sprite_fetch_coordinates(location, sprite) \
|
||||
sprite_fetch_graphics(location+2, sprite)
|
||||
|
||||
#define sprite_y_read(location, sprite) \
|
||||
slot(location): posit_sprite(sprite_selection_buffer, sprite, ram_[sprite_attribute_table_address_ & (((sprite) << 2) | 0x3f80)], write_pointer_.row);
|
||||
|
||||
#define fetch_tile_name(column) line_buffer.names[column].offset = ram_[(row_base + column) & 0x3fff];
|
||||
|
||||
#define fetch_tile(column) {\
|
||||
line_buffer.patterns[column][1] = ram_[(colour_base + size_t((line_buffer.names[column].offset << 3) >> colour_name_shift)) & 0x3fff]; \
|
||||
line_buffer.patterns[column][0] = ram_[(pattern_base + size_t(line_buffer.names[column].offset << 3)) & 0x3fff]; \
|
||||
}
|
||||
|
||||
#define background_fetch_block(location, column, sprite) \
|
||||
slot(location): fetch_tile_name(column) \
|
||||
external_slot(location+1); \
|
||||
slot(location+2): \
|
||||
slot(location+3): fetch_tile(column) \
|
||||
slot(location+4): fetch_tile_name(column+1) \
|
||||
sprite_y_read(location+5, sprite); \
|
||||
slot(location+6): \
|
||||
slot(location+7): fetch_tile(column+1) \
|
||||
slot(location+8): fetch_tile_name(column+2) \
|
||||
sprite_y_read(location+9, sprite+1); \
|
||||
slot(location+10): \
|
||||
slot(location+11): fetch_tile(column+2) \
|
||||
slot(location+12): fetch_tile_name(column+3) \
|
||||
sprite_y_read(location+13, sprite+2); \
|
||||
slot(location+14): \
|
||||
slot(location+15): fetch_tile(column+3)
|
||||
|
||||
LineBuffer &line_buffer = line_buffers_[write_pointer_.row];
|
||||
LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines];
|
||||
const size_t row_base = pattern_name_address_ & (size_t((write_pointer_.row << 2)&~31) | 0x3c00);
|
||||
|
||||
size_t pattern_base = pattern_generator_table_address_;
|
||||
size_t colour_base = colour_table_address_;
|
||||
int colour_name_shift = 6;
|
||||
|
||||
if(screen_mode_ == ScreenMode::Graphics) {
|
||||
// If this is high resolution mode, allow the row number to affect the pattern and colour addresses.
|
||||
pattern_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5));
|
||||
colour_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5));
|
||||
|
||||
colour_base += size_t(write_pointer_.row & 7);
|
||||
colour_name_shift = 0;
|
||||
} else {
|
||||
colour_base &= size_t(0xffc0);
|
||||
pattern_base &= size_t(0x3800);
|
||||
}
|
||||
|
||||
if(screen_mode_ == ScreenMode::MultiColour) {
|
||||
pattern_base += size_t((write_pointer_.row >> 2) & 7);
|
||||
} else {
|
||||
pattern_base += size_t(write_pointer_.row & 7);
|
||||
}
|
||||
|
||||
switch(start) {
|
||||
default: assert(false);
|
||||
|
||||
external_slots_2(0);
|
||||
|
||||
sprite_fetch_block(2, 0);
|
||||
sprite_fetch_block(8, 1);
|
||||
sprite_fetch_coordinates(14, 2);
|
||||
|
||||
external_slots_4(16);
|
||||
external_slot(20);
|
||||
|
||||
sprite_fetch_graphics(21, 2);
|
||||
sprite_fetch_block(25, 3);
|
||||
|
||||
slot(31):
|
||||
sprite_selection_buffer.reset_sprite_collection();
|
||||
do_external_slot(31*2);
|
||||
external_slots_2(32);
|
||||
external_slot(34);
|
||||
|
||||
sprite_y_read(35, 0);
|
||||
sprite_y_read(36, 1);
|
||||
sprite_y_read(37, 2);
|
||||
sprite_y_read(38, 3);
|
||||
sprite_y_read(39, 4);
|
||||
sprite_y_read(40, 5);
|
||||
sprite_y_read(41, 6);
|
||||
sprite_y_read(42, 7);
|
||||
|
||||
background_fetch_block(43, 0, 8);
|
||||
background_fetch_block(59, 4, 11);
|
||||
background_fetch_block(75, 8, 14);
|
||||
background_fetch_block(91, 12, 17);
|
||||
background_fetch_block(107, 16, 20);
|
||||
background_fetch_block(123, 20, 23);
|
||||
background_fetch_block(139, 24, 26);
|
||||
background_fetch_block(155, 28, 29);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#undef background_fetch_block
|
||||
#undef fetch_tile
|
||||
#undef fetch_tile_name
|
||||
#undef sprite_y_read
|
||||
#undef sprite_fetch_block
|
||||
#undef sprite_fetch_graphics
|
||||
#undef sprite_fetch_coordinates
|
||||
}
|
||||
|
||||
|
||||
/***********************************************
|
||||
Master System Fetching Code
|
||||
************************************************/
|
||||
|
||||
template<bool use_end> void fetch_sms(int start, int end) {
|
||||
#define sprite_fetch(sprite) {\
|
||||
line_buffer.active_sprites[sprite].x = \
|
||||
ram_[\
|
||||
master_system_.sprite_attribute_table_address & size_t(0x3f80 | (line_buffer.active_sprites[sprite].index << 1))\
|
||||
] - (master_system_.shift_sprites_8px_left ? 8 : 0); \
|
||||
const uint8_t name = ram_[\
|
||||
master_system_.sprite_attribute_table_address & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 1))\
|
||||
] & (sprites_16x16_ ? ~1 : ~0);\
|
||||
const size_t graphic_location = master_system_.sprite_generator_table_address & size_t(0x2000 | (name << 5) | (line_buffer.active_sprites[sprite].row << 2)); \
|
||||
line_buffer.active_sprites[sprite].image[0] = ram_[graphic_location]; \
|
||||
line_buffer.active_sprites[sprite].image[1] = ram_[graphic_location+1]; \
|
||||
line_buffer.active_sprites[sprite].image[2] = ram_[graphic_location+2]; \
|
||||
line_buffer.active_sprites[sprite].image[3] = ram_[graphic_location+3]; \
|
||||
}
|
||||
|
||||
#define sprite_fetch_block(location, sprite) \
|
||||
slot(location): \
|
||||
slot(location+1): \
|
||||
slot(location+2): \
|
||||
slot(location+3): \
|
||||
slot(location+4): \
|
||||
slot(location+5): \
|
||||
sprite_fetch(sprite);\
|
||||
sprite_fetch(sprite+1);
|
||||
|
||||
#define sprite_y_read(location, sprite) \
|
||||
slot(location): \
|
||||
posit_sprite(sprite_selection_buffer, sprite, ram_[master_system_.sprite_attribute_table_address & ((sprite) | 0x3f00)], write_pointer_.row); \
|
||||
posit_sprite(sprite_selection_buffer, sprite+1, ram_[master_system_.sprite_attribute_table_address & ((sprite + 1) | 0x3f00)], write_pointer_.row); \
|
||||
|
||||
#define fetch_tile_name(column, row_info) {\
|
||||
const size_t scrolled_column = (column - horizontal_offset) & 0x1f;\
|
||||
const size_t address = row_info.pattern_address_base + (scrolled_column << 1); \
|
||||
line_buffer.names[column].flags = ram_[address+1]; \
|
||||
line_buffer.names[column].offset = static_cast<size_t>( \
|
||||
(((line_buffer.names[column].flags&1) << 8) | ram_[address]) << 5 \
|
||||
) + row_info.sub_row[(line_buffer.names[column].flags&4) >> 2]; \
|
||||
}
|
||||
|
||||
#define fetch_tile(column) \
|
||||
line_buffer.patterns[column][0] = ram_[line_buffer.names[column].offset]; \
|
||||
line_buffer.patterns[column][1] = ram_[line_buffer.names[column].offset+1]; \
|
||||
line_buffer.patterns[column][2] = ram_[line_buffer.names[column].offset+2]; \
|
||||
line_buffer.patterns[column][3] = ram_[line_buffer.names[column].offset+3];
|
||||
|
||||
#define background_fetch_block(location, column, sprite, row_info) \
|
||||
slot(location): fetch_tile_name(column, row_info) \
|
||||
external_slot(location+1); \
|
||||
slot(location+2): \
|
||||
slot(location+3): \
|
||||
slot(location+4): \
|
||||
fetch_tile(column) \
|
||||
fetch_tile_name(column+1, row_info) \
|
||||
sprite_y_read(location+5, sprite); \
|
||||
slot(location+6): \
|
||||
slot(location+7): \
|
||||
slot(location+8): \
|
||||
fetch_tile(column+1) \
|
||||
fetch_tile_name(column+2, row_info) \
|
||||
sprite_y_read(location+9, sprite+2); \
|
||||
slot(location+10): \
|
||||
slot(location+11): \
|
||||
slot(location+12): \
|
||||
fetch_tile(column+2) \
|
||||
fetch_tile_name(column+3, row_info) \
|
||||
sprite_y_read(location+13, sprite+4); \
|
||||
slot(location+14): \
|
||||
slot(location+15): fetch_tile(column+3)
|
||||
|
||||
// Determine the coarse horizontal scrolling offset; this isn't applied on the first two lines if the programmer has requested it.
|
||||
LineBuffer &line_buffer = line_buffers_[write_pointer_.row];
|
||||
LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines];
|
||||
const int horizontal_offset = (write_pointer_.row >= 16 || !master_system_.horizontal_scroll_lock) ? (line_buffer.latched_horizontal_scroll >> 3) : 0;
|
||||
|
||||
// Limit address bits in use if this is a SMS2 mode.
|
||||
const bool is_tall_mode = mode_timing_.pixel_lines != 192;
|
||||
const size_t pattern_name_address = master_system_.pattern_name_address | (is_tall_mode ? 0x800 : 0);
|
||||
const size_t pattern_name_offset = is_tall_mode ? 0x100 : 0;
|
||||
|
||||
// Determine row info for the screen both (i) if vertical scrolling is applied; and (ii) if it isn't.
|
||||
// The programmer can opt out of applying vertical scrolling to the right-hand portion of the display.
|
||||
const int scrolled_row = (write_pointer_.row + master_system_.latched_vertical_scroll) % (is_tall_mode ? 256 : 224);
|
||||
struct RowInfo {
|
||||
size_t pattern_address_base;
|
||||
size_t sub_row[2];
|
||||
};
|
||||
const RowInfo scrolled_row_info = {
|
||||
(pattern_name_address & size_t(((scrolled_row & ~7) << 3) | 0x3800)) - pattern_name_offset,
|
||||
{static_cast<size_t>((scrolled_row & 7) << 2), 28 ^ static_cast<size_t>((scrolled_row & 7) << 2)}
|
||||
};
|
||||
RowInfo row_info;
|
||||
if(master_system_.vertical_scroll_lock) {
|
||||
row_info.pattern_address_base = (pattern_name_address & size_t(((write_pointer_.row & ~7) << 3) | 0x3800)) - pattern_name_offset;
|
||||
row_info.sub_row[0] = size_t((write_pointer_.row & 7) << 2);
|
||||
row_info.sub_row[1] = 28 ^ size_t((write_pointer_.row & 7) << 2);
|
||||
} else row_info = scrolled_row_info;
|
||||
|
||||
// ... and do the actual fetching, which follows this routine:
|
||||
switch(start) {
|
||||
default: assert(false);
|
||||
|
||||
sprite_fetch_block(0, 0);
|
||||
sprite_fetch_block(6, 2);
|
||||
|
||||
external_slots_4(12);
|
||||
external_slot(16);
|
||||
|
||||
sprite_fetch_block(17, 4);
|
||||
sprite_fetch_block(23, 6);
|
||||
|
||||
slot(29):
|
||||
sprite_selection_buffer.reset_sprite_collection();
|
||||
do_external_slot(29*2);
|
||||
external_slot(30);
|
||||
|
||||
sprite_y_read(31, 0);
|
||||
sprite_y_read(32, 2);
|
||||
sprite_y_read(33, 4);
|
||||
sprite_y_read(34, 6);
|
||||
sprite_y_read(35, 8);
|
||||
sprite_y_read(36, 10);
|
||||
sprite_y_read(37, 12);
|
||||
sprite_y_read(38, 14);
|
||||
|
||||
background_fetch_block(39, 0, 16, scrolled_row_info);
|
||||
background_fetch_block(55, 4, 22, scrolled_row_info);
|
||||
background_fetch_block(71, 8, 28, scrolled_row_info);
|
||||
background_fetch_block(87, 12, 34, scrolled_row_info);
|
||||
background_fetch_block(103, 16, 40, scrolled_row_info);
|
||||
background_fetch_block(119, 20, 46, scrolled_row_info);
|
||||
background_fetch_block(135, 24, 52, row_info);
|
||||
background_fetch_block(151, 28, 58, row_info);
|
||||
|
||||
external_slots_4(167);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#undef background_fetch_block
|
||||
#undef fetch_tile
|
||||
#undef fetch_tile_name
|
||||
#undef sprite_y_read
|
||||
#undef sprite_fetch_block
|
||||
#undef sprite_fetch
|
||||
}
|
||||
|
||||
#undef external_slot
|
||||
#undef slot
|
||||
|
||||
uint32_t *pixel_target_ = nullptr, *pixel_origin_ = nullptr;
|
||||
bool asked_for_write_area_ = false;
|
||||
void draw_tms_character(int start, int end);
|
||||
void draw_tms_text(int start, int end);
|
||||
void draw_sms(int start, int end, uint32_t cram_dot);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* TMS9918Base_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
|
||||
@@ -253,7 +291,7 @@ uint8_t AY38910::get_data_output() {
|
||||
const uint8_t mask = port_handler_ ? port_handler_->get_port_input(selected_register_ == 15) : 0xff;
|
||||
|
||||
switch(selected_register_) {
|
||||
default: break;
|
||||
default: break;
|
||||
case 14: return mask & ((registers_[0x7] & 0x40) ? registers_[14] : 0xff);
|
||||
case 15: return mask & ((registers_[0x7] & 0x80) ? registers_[15] : 0xff);
|
||||
}
|
||||
@@ -280,7 +318,7 @@ void AY38910::update_bus() {
|
||||
// Assume no output, unless this turns out to be a read.
|
||||
data_output_ = 0xff;
|
||||
switch(control_state_) {
|
||||
default: break;
|
||||
default: break;
|
||||
case LatchAddress: select_register(data_input_); break;
|
||||
case Write: set_register_value(data_input_); break;
|
||||
case Read: data_output_ = get_register_value(); break;
|
||||
|
||||
@@ -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);
|
||||
@@ -73,13 +73,18 @@ void DiskII::select_drive(int drive) {
|
||||
drives_[active_drive_].set_motor_on(motor_is_enabled_);
|
||||
}
|
||||
|
||||
// The read pulse is controlled by a special IC that outputs a 1us pulse for every field reversal on the disk.
|
||||
|
||||
void DiskII::run_for(const Cycles cycles) {
|
||||
if(preferred_clocking() == ClockingHint::Preference::None) return;
|
||||
|
||||
int integer_cycles = cycles.as_int();
|
||||
while(integer_cycles--) {
|
||||
const int address = (state_ & 0xf0) | inputs_ | ((shift_register_&0x80) >> 6);
|
||||
inputs_ |= input_flux;
|
||||
if(flux_duration_) {
|
||||
--flux_duration_;
|
||||
if(!flux_duration_) inputs_ |= input_flux;
|
||||
}
|
||||
state_ = state_machine_[static_cast<std::size_t>(address)];
|
||||
switch(state_ & 0xf) {
|
||||
default: shift_register_ = 0; break; // clear
|
||||
@@ -115,6 +120,15 @@ void DiskII::run_for(const Cycles cycles) {
|
||||
if(!drive_is_sleeping_[1]) drives_[1].run_for(Cycles(1));
|
||||
}
|
||||
|
||||
// Per comp.sys.apple2.programmer there is a delay between the controller
|
||||
// motor switch being flipped and the drive motor actually switching off.
|
||||
// This models that, accepting overrun as a risk.
|
||||
if(motor_off_time_ >= 0) {
|
||||
motor_off_time_ -= cycles.as_int();
|
||||
if(motor_off_time_ < 0) {
|
||||
set_control(Control::Motor, false);
|
||||
}
|
||||
}
|
||||
decide_clocking_preference();
|
||||
}
|
||||
|
||||
@@ -197,9 +211,10 @@ 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.
|
||||
decide_clocking_preference();
|
||||
}
|
||||
}
|
||||
@@ -232,9 +247,12 @@ int DiskII::read_address(int address) {
|
||||
|
||||
case 0x8:
|
||||
shift_register_ = 0;
|
||||
set_control(Control::Motor, false);
|
||||
motor_off_time_ = clock_rate_;
|
||||
break;
|
||||
case 0x9:
|
||||
set_control(Control::Motor, true);
|
||||
motor_off_time_ = -1;
|
||||
break;
|
||||
case 0x9: set_control(Control::Motor, true); break;
|
||||
|
||||
case 0xa: select_drive(0); break;
|
||||
case 0xb: select_drive(1); break;
|
||||
|
||||
@@ -54,7 +54,7 @@ class DiskII:
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
/*!
|
||||
Supplies the image of the state machine (i.e. P6) ROM,
|
||||
Supplies the image of the state machine (i.e. P6) ROM,
|
||||
which dictates how the Disk II will respond to input.
|
||||
|
||||
To reduce processing costs, some assumptions are made by
|
||||
@@ -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;
|
||||
@@ -109,6 +109,7 @@ class DiskII:
|
||||
|
||||
int stepper_mask_ = 0;
|
||||
int stepper_position_ = 0;
|
||||
int motor_off_time_ = -1;
|
||||
|
||||
bool is_write_protected();
|
||||
std::array<uint8_t, 256> state_machine_;
|
||||
@@ -121,6 +122,7 @@ class DiskII:
|
||||
ClockingHint::Preference clocking_preference_ = ClockingHint::Preference::RealTime;
|
||||
|
||||
uint8_t data_input_ = 0;
|
||||
int flux_duration_ = 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 */
|
||||
@@ -19,7 +19,7 @@ ListSelection *ListSelection::list_selection() {
|
||||
}
|
||||
|
||||
BooleanSelection *ListSelection::boolean_selection() {
|
||||
return new BooleanSelection(value != "no" && value != "n");
|
||||
return new BooleanSelection(value != "no" && value != "n" && value != "false" && value != "f");
|
||||
}
|
||||
|
||||
BooleanSelection *BooleanSelection::boolean_selection() {
|
||||
|
||||
@@ -69,7 +69,7 @@ struct BooleanSelection: public Selection {
|
||||
|
||||
struct ListSelection: public Selection {
|
||||
std::string value;
|
||||
|
||||
|
||||
ListSelection *list_selection();
|
||||
BooleanSelection *boolean_selection();
|
||||
ListSelection(const std::string value) : value(value) {}
|
||||
|
||||
@@ -33,11 +33,12 @@ bool get_bool(const Configurable::SelectionSet &selections_by_option, const std:
|
||||
std::vector<std::unique_ptr<Configurable::Option>> Configurable::standard_options(Configurable::StandardOptions mask) {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> options;
|
||||
if(mask & QuickLoadTape) options.emplace_back(new Configurable::BooleanOption("Load Tapes Quickly", "quickload"));
|
||||
if(mask & (DisplayRGB | DisplayComposite | DisplaySVideo)) {
|
||||
if(mask & (DisplayRGB | DisplayCompositeColour | DisplayCompositeMonochrome | DisplaySVideo)) {
|
||||
std::vector<std::string> display_options;
|
||||
if(mask & DisplayComposite) display_options.emplace_back("composite");
|
||||
if(mask & DisplaySVideo) display_options.emplace_back("svideo");
|
||||
if(mask & DisplayRGB) display_options.emplace_back("rgb");
|
||||
if(mask & DisplayCompositeColour) display_options.emplace_back("composite");
|
||||
if(mask & DisplayCompositeMonochrome) display_options.emplace_back("composite-mono");
|
||||
if(mask & DisplaySVideo) display_options.emplace_back("svideo");
|
||||
if(mask & DisplayRGB) display_options.emplace_back("rgb");
|
||||
options.emplace_back(new Configurable::ListOption("Display", "display", display_options));
|
||||
}
|
||||
if(mask & AutomaticTapeMotorControl) options.emplace_back(new Configurable::BooleanOption("Automatic Tape Motor Control", "autotapemotor"));
|
||||
@@ -57,9 +58,10 @@ void Configurable::append_display_selection(Configurable::SelectionSet &selectio
|
||||
std::string string_selection;
|
||||
switch(selection) {
|
||||
default:
|
||||
case Display::RGB: string_selection = "rgb"; break;
|
||||
case Display::SVideo: string_selection = "svideo"; break;
|
||||
case Display::Composite: string_selection = "composite"; break;
|
||||
case Display::RGB: string_selection = "rgb"; break;
|
||||
case Display::SVideo: string_selection = "svideo"; break;
|
||||
case Display::CompositeMonochrome: string_selection = "composite-mono"; break;
|
||||
case Display::CompositeColour: string_selection = "composite"; break;
|
||||
}
|
||||
selection_set["display"] = std::unique_ptr<Configurable::Selection>(new Configurable::ListSelection(string_selection));
|
||||
}
|
||||
@@ -85,7 +87,11 @@ bool Configurable::get_display(const Configurable::SelectionSet &selections_by_o
|
||||
return true;
|
||||
}
|
||||
if(display->value == "composite") {
|
||||
result = Configurable::Display::Composite;
|
||||
result = Configurable::Display::CompositeColour;
|
||||
return true;
|
||||
}
|
||||
if(display->value == "composite-mono") {
|
||||
result = Configurable::Display::CompositeMonochrome;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,17 @@ namespace Configurable {
|
||||
enum StandardOptions {
|
||||
DisplayRGB = (1 << 0),
|
||||
DisplaySVideo = (1 << 1),
|
||||
DisplayComposite = (1 << 2),
|
||||
QuickLoadTape = (1 << 3),
|
||||
AutomaticTapeMotorControl = (1 << 4)
|
||||
DisplayCompositeColour = (1 << 2),
|
||||
DisplayCompositeMonochrome = (1 << 3),
|
||||
QuickLoadTape = (1 << 4),
|
||||
AutomaticTapeMotorControl = (1 << 5)
|
||||
};
|
||||
|
||||
enum class Display {
|
||||
RGB,
|
||||
SVideo,
|
||||
Composite
|
||||
CompositeColour,
|
||||
CompositeMonochrome
|
||||
};
|
||||
|
||||
/*!
|
||||
|
||||
@@ -128,6 +128,24 @@ class Joystick {
|
||||
set_input(input, 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Gets the number of input fire buttons.
|
||||
|
||||
This is cached by default, but it's virtual so overridable.
|
||||
*/
|
||||
virtual int get_number_of_fire_buttons() {
|
||||
if(number_of_buttons_ >= 0) return number_of_buttons_;
|
||||
|
||||
number_of_buttons_ = 0;
|
||||
for(const auto &input: get_inputs()) {
|
||||
if(input.type == Input::Type::Fire) ++number_of_buttons_;
|
||||
}
|
||||
return number_of_buttons_;
|
||||
}
|
||||
|
||||
private:
|
||||
int number_of_buttons_ = -1;
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -167,11 +185,11 @@ class ConcreteJoystick: public Joystick {
|
||||
// convenient hard-coded values. TODO: make these a function of time.
|
||||
using Type = Joystick::Input::Type;
|
||||
switch(input.type) {
|
||||
default: did_set_input(input, is_active ? 1.0f : 0.0f); break;
|
||||
case Type::Left: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.25f : 0.5f); break;
|
||||
case Type::Right: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.75f : 0.5f); break;
|
||||
case Type::Up: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.25f : 0.5f); break;
|
||||
case Type::Down: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.75f : 0.5f); break;
|
||||
default: did_set_input(input, is_active ? 1.0f : 0.0f); break;
|
||||
case Type::Left: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.1f : 0.5f); break;
|
||||
case Type::Right: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.9f : 0.5f); break;
|
||||
case Type::Up: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.1f : 0.5f); break;
|
||||
case Type::Down: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.9f : 0.5f); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +203,7 @@ class ConcreteJoystick: public Joystick {
|
||||
// Otherwise apply a threshold test to convert to digital, with remapping from axes to digital inputs.
|
||||
using Type = Joystick::Input::Type;
|
||||
switch(input.type) {
|
||||
default: did_set_input(input, value > 0.5f); break;
|
||||
default: did_set_input(input, value > 0.5f); break;
|
||||
case Type::Horizontal:
|
||||
did_set_input(Input(Type::Left, input.info.control.index), value <= 0.25f);
|
||||
did_set_input(Input(Type::Right, input.info.control.index), value >= 0.75f);
|
||||
|
||||
@@ -10,7 +10,13 @@
|
||||
|
||||
using namespace Inputs;
|
||||
|
||||
Keyboard::Keyboard() {}
|
||||
Keyboard::Keyboard() {
|
||||
for(int k = 0; k < int(Key::Help); ++k) {
|
||||
observed_keys_.insert(Key(k));
|
||||
}
|
||||
}
|
||||
|
||||
Keyboard::Keyboard(const std::set<Key> &observed_keys) : observed_keys_(observed_keys), is_exclusive_(false) {}
|
||||
|
||||
void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) {
|
||||
std::size_t key_offset = static_cast<std::size_t>(key);
|
||||
@@ -36,3 +42,11 @@ bool Keyboard::get_key_state(Key key) {
|
||||
if(key_offset >= key_states_.size()) return false;
|
||||
return key_states_[key_offset];
|
||||
}
|
||||
|
||||
const std::set<Keyboard::Key> &Keyboard::observed_keys() {
|
||||
return observed_keys_;
|
||||
}
|
||||
|
||||
bool Keyboard::is_exclusive() {
|
||||
return is_exclusive_;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
// 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>
|
||||
|
||||
namespace Inputs {
|
||||
|
||||
@@ -20,8 +21,6 @@ namespace Inputs {
|
||||
*/
|
||||
class Keyboard {
|
||||
public:
|
||||
Keyboard();
|
||||
|
||||
enum class Key {
|
||||
Escape, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, PrintScreen, ScrollLock, Pause,
|
||||
BackTick, k1, k2, k3, k4, k5, k6, k7, k8, k9, k0, Hyphen, Equals, BackSpace,
|
||||
@@ -39,10 +38,26 @@ class Keyboard {
|
||||
Help
|
||||
};
|
||||
|
||||
/// Constructs a Keyboard that declares itself to observe all keys.
|
||||
Keyboard();
|
||||
|
||||
/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys.
|
||||
Keyboard(const std::set<Key> &observed_keys);
|
||||
|
||||
// Host interface.
|
||||
virtual void set_key_pressed(Key key, char value, bool is_pressed);
|
||||
virtual void reset_all_keys();
|
||||
|
||||
/// @returns a set of all Keys that this keyboard responds to.
|
||||
virtual const std::set<Key> &observed_keys();
|
||||
|
||||
/*
|
||||
@returns @c true if this keyboard, on its original machine, looked
|
||||
like a complete keyboard — i.e. if a user would expect this keyboard
|
||||
to be the only thing a real keyboard maps to.
|
||||
*/
|
||||
virtual bool is_exclusive();
|
||||
|
||||
// Delegate interface.
|
||||
struct Delegate {
|
||||
virtual void keyboard_did_change_key(Keyboard *keyboard, Key key, bool is_pressed) = 0;
|
||||
@@ -52,10 +67,12 @@ class Keyboard {
|
||||
bool get_key_state(Key key);
|
||||
|
||||
private:
|
||||
std::set<Key> observed_keys_;
|
||||
std::vector<bool> key_states_;
|
||||
Delegate *delegate_ = nullptr;
|
||||
bool is_exclusive_ = true;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#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 */
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
#include "../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../Outputs/CRT/CRT.hpp"
|
||||
|
||||
#include "../../Analyser/Static/AmstradCPC/Target.hpp"
|
||||
|
||||
@@ -40,7 +41,7 @@ namespace AmstradCPC {
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
return Configurable::standard_options(
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayComposite)
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayCompositeColour)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,10 +172,14 @@ class AYDeferrer {
|
||||
class CRTCBusHandler {
|
||||
public:
|
||||
CRTCBusHandler(uint8_t *ram, InterruptTimer &interrupt_timer) :
|
||||
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2),
|
||||
ram_(ram),
|
||||
interrupt_timer_(interrupt_timer) {
|
||||
establish_palette_hits();
|
||||
build_mode_table();
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f));
|
||||
crt_.set_brightness(3.0f / 2.0f); // As only the values 0, 1 and 2 will be used in each channel,
|
||||
// whereas Red2Green2Blue2 defines a range of 0-3.
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -217,12 +222,12 @@ class CRTCBusHandler {
|
||||
if(cycles_) {
|
||||
switch(previous_output_mode_) {
|
||||
default:
|
||||
case OutputMode::Blank: crt_->output_blank(cycles_ * 16); break;
|
||||
case OutputMode::Sync: crt_->output_sync(cycles_ * 16); break;
|
||||
case OutputMode::Blank: crt_.output_blank(cycles_ * 16); break;
|
||||
case OutputMode::Sync: crt_.output_sync(cycles_ * 16); break;
|
||||
case OutputMode::Border: output_border(cycles_); break;
|
||||
case OutputMode::ColourBurst: crt_->output_default_colour_burst(cycles_ * 16); break;
|
||||
case OutputMode::ColourBurst: crt_.output_default_colour_burst(cycles_ * 16); break;
|
||||
case OutputMode::Pixels:
|
||||
crt_->output_data(cycles_ * 16, cycles_ * 16 / pixel_divider_);
|
||||
crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_));
|
||||
pixel_pointer_ = pixel_data_ = nullptr;
|
||||
break;
|
||||
}
|
||||
@@ -238,7 +243,7 @@ class CRTCBusHandler {
|
||||
// collect some more pixels if output is ongoing
|
||||
if(previous_output_mode_ == OutputMode::Pixels) {
|
||||
if(!pixel_data_) {
|
||||
pixel_pointer_ = pixel_data_ = crt_->allocate_write_area(320, 8);
|
||||
pixel_pointer_ = pixel_data_ = crt_.begin_data(320, 8);
|
||||
}
|
||||
if(pixel_pointer_) {
|
||||
// the CPC shuffles output lines as:
|
||||
@@ -283,7 +288,7 @@ class CRTCBusHandler {
|
||||
// widths so it's not necessarily possible to predict the correct number in advance
|
||||
// and using the upper bound could lead to inefficient behaviour
|
||||
if(pixel_pointer_ == pixel_data_ + 320) {
|
||||
crt_->output_data(cycles_ * 16, cycles_ * 16 / pixel_divider_);
|
||||
crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_));
|
||||
pixel_pointer_ = pixel_data_ = nullptr;
|
||||
cycles_ = 0;
|
||||
}
|
||||
@@ -323,27 +328,14 @@ class CRTCBusHandler {
|
||||
was_hsync_ = state.hsync;
|
||||
}
|
||||
|
||||
/// Constructs an appropriate CRT for video output.
|
||||
void setup_output(float aspect_ratio) {
|
||||
crt_.reset(new Outputs::CRT::CRT(1024, 16, Outputs::CRT::DisplayType::PAL50, 1));
|
||||
crt_->set_rgb_sampling_function(
|
||||
"vec3 rgb_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate)"
|
||||
"{"
|
||||
"uint sample = texture(texID, coordinate).r;"
|
||||
"return vec3(float((sample >> 4) & 3u), float((sample >> 2) & 3u), float(sample & 3u)) / 2.0;"
|
||||
"}");
|
||||
crt_->set_visible_area(Outputs::CRT::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f));
|
||||
crt_->set_video_signal(Outputs::CRT::VideoSignal::RGB);
|
||||
/// Sets the destination for output.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// Destructs the CRT.
|
||||
void close_output() {
|
||||
crt_.reset();
|
||||
}
|
||||
|
||||
/// @returns the CRT.
|
||||
Outputs::CRT::CRT *get_crt() {
|
||||
return crt_.get();
|
||||
/// Sets the type of display.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -376,10 +368,10 @@ class CRTCBusHandler {
|
||||
}
|
||||
|
||||
private:
|
||||
void output_border(unsigned int length) {
|
||||
uint8_t *colour_pointer = static_cast<uint8_t *>(crt_->allocate_write_area(1));
|
||||
void output_border(int length) {
|
||||
uint8_t *colour_pointer = static_cast<uint8_t *>(crt_.begin_data(1));
|
||||
if(colour_pointer) *colour_pointer = border_;
|
||||
crt_->output_level(length * 16);
|
||||
crt_.output_level(length * 16);
|
||||
}
|
||||
|
||||
#define Mode0Colour0(c) ((c & 0x80) >> 7) | ((c & 0x20) >> 3) | ((c & 0x08) >> 2) | ((c & 0x02) << 2)
|
||||
@@ -528,19 +520,19 @@ class CRTCBusHandler {
|
||||
Border,
|
||||
Pixels
|
||||
} previous_output_mode_ = OutputMode::Sync;
|
||||
unsigned int cycles_ = 0;
|
||||
int cycles_ = 0;
|
||||
|
||||
bool was_hsync_ = false, was_vsync_ = false;
|
||||
int cycles_into_hsync_ = 0;
|
||||
|
||||
std::unique_ptr<Outputs::CRT::CRT> crt_;
|
||||
Outputs::CRT::CRT crt_;
|
||||
uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr;
|
||||
|
||||
uint8_t *ram_ = nullptr;
|
||||
|
||||
int next_mode_ = 2, mode_ = 2;
|
||||
|
||||
unsigned int pixel_divider_ = 1;
|
||||
int pixel_divider_ = 1;
|
||||
uint16_t mode0_output_[256];
|
||||
uint32_t mode1_output_[256];
|
||||
uint64_t mode2_output_[256];
|
||||
@@ -757,7 +749,7 @@ class i8255PortHandler : public Intel::i8255::PortHandler {
|
||||
template <bool has_fdc> class ConcreteMachine:
|
||||
public CRTMachine::Machine,
|
||||
public MediaTarget::Machine,
|
||||
public KeyboardMachine::Machine,
|
||||
public KeyboardMachine::MappedMachine,
|
||||
public Utility::TypeRecipient,
|
||||
public CPU::Z80::BusHandler,
|
||||
public ClockingHint::Observer,
|
||||
@@ -789,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];
|
||||
@@ -981,19 +983,14 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
flush_fdc();
|
||||
}
|
||||
|
||||
/// A CRTMachine function; indicates that outputs should be created now.
|
||||
void setup_output(float aspect_ratio) override final {
|
||||
crtc_bus_handler_.setup_output(aspect_ratio);
|
||||
/// A CRTMachine function; sets the destination for video.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
|
||||
crtc_bus_handler_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// A CRTMachine function; indicates that outputs should be destroyed now.
|
||||
void close_output() override final {
|
||||
crtc_bus_handler_.close_output();
|
||||
}
|
||||
|
||||
/// @returns the CRT in use.
|
||||
Outputs::CRT::CRT *get_crt() override final {
|
||||
return crtc_bus_handler_.get_crt();
|
||||
/// A CRTMachine function; sets the output display type.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override final {
|
||||
crtc_bus_handler_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
/// @returns the speaker in use.
|
||||
@@ -1192,7 +1189,7 @@ Machine *Machine::AmstradCPC(const Analyser::Static::Target *target, const ROMMa
|
||||
using Target = Analyser::Static::AmstradCPC::Target;
|
||||
const Target *const cpc_target = dynamic_cast<const Target *>(target);
|
||||
switch(cpc_target->model) {
|
||||
default: return new AmstradCPC::ConcreteMachine<true>(*cpc_target, rom_fetcher);
|
||||
default: return new AmstradCPC::ConcreteMachine<true>(*cpc_target, rom_fetcher);
|
||||
case Target::Model::CPC464: return new AmstradCPC::ConcreteMachine<false>(*cpc_target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +75,9 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
}
|
||||
|
||||
uint16_t *CharacterMapper::sequence_for_character(char character) {
|
||||
#define KEYS(...) {__VA_ARGS__, KeyboardMachine::Machine::KeyEndSequence}
|
||||
#define SHIFT(...) {KeyShift, __VA_ARGS__, KeyboardMachine::Machine::KeyEndSequence}
|
||||
#define X {KeyboardMachine::Machine::KeyNotMapped}
|
||||
#define KEYS(...) {__VA_ARGS__, KeyboardMachine::MappedMachine::KeyEndSequence}
|
||||
#define SHIFT(...) {KeyShift, __VA_ARGS__, KeyboardMachine::MappedMachine::KeyEndSequence}
|
||||
#define X {KeyboardMachine::MappedMachine::KeyNotMapped}
|
||||
static KeySequence key_sequences[] = {
|
||||
/* NUL */ X, /* SOH */ X,
|
||||
/* STX */ X, /* ETX */ X,
|
||||
|
||||
@@ -33,7 +33,7 @@ enum Key: uint16_t {
|
||||
#undef Line
|
||||
};
|
||||
|
||||
struct KeyboardMapper: public KeyboardMachine::Machine::KeyboardMapper {
|
||||
struct KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key);
|
||||
};
|
||||
|
||||
|
||||
885
Machines/Apple/AppleII/AppleII.cpp
Normal file
885
Machines/Apple/AppleII/AppleII.cpp
Normal file
@@ -0,0 +1,885 @@
|
||||
//
|
||||
// AppleII.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/04/2018.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#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 "../../../Processors/6502/6502.hpp"
|
||||
#include "../../../Components/AudioToggle/AudioToggle.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 <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
return Configurable::standard_options(
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplayCompositeMonochrome | Configurable::DisplayCompositeColour)
|
||||
);
|
||||
}
|
||||
|
||||
#define is_iie() ((model == Analyser::Static::AppleII::Target::Model::IIe) || (model == Analyser::Static::AppleII::Target::Model::EnhancedIIe))
|
||||
|
||||
template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
public CRTMachine::Machine,
|
||||
public MediaTarget::Machine,
|
||||
public KeyboardMachine::MappedMachine,
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public Inputs::Keyboard,
|
||||
public Configurable::Device,
|
||||
public Apple::II::Machine,
|
||||
public Activity::Source,
|
||||
public JoystickMachine::Machine,
|
||||
public Apple::II::Card::Delegate {
|
||||
private:
|
||||
struct VideoBusHandler : public Apple::II::Video::BusHandler {
|
||||
public:
|
||||
VideoBusHandler(uint8_t *ram, uint8_t *aux_ram) : ram_(ram), aux_ram_(aux_ram) {}
|
||||
|
||||
void perform_read(uint16_t address, size_t count, uint8_t *base_target, uint8_t *auxiliary_target) {
|
||||
memcpy(base_target, &ram_[address], count);
|
||||
memcpy(auxiliary_target, &aux_ram_[address], count);
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t *ram_, *aux_ram_;
|
||||
};
|
||||
|
||||
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_;
|
||||
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<Cycles>());
|
||||
}
|
||||
static const int audio_divider = 8;
|
||||
void update_audio() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider)));
|
||||
}
|
||||
void update_just_in_time_cards() {
|
||||
for(const auto &card : just_in_time_cards_) {
|
||||
card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_);
|
||||
}
|
||||
cycles_since_card_update_ = 0;
|
||||
stretched_cycles_since_card_update_ = 0;
|
||||
}
|
||||
|
||||
uint8_t ram_[65536], aux_ram_[65536];
|
||||
std::vector<uint8_t> rom_;
|
||||
uint8_t keyboard_input_ = 0x00;
|
||||
bool key_is_down_ = false;
|
||||
|
||||
uint8_t get_keyboard_input() {
|
||||
if(string_serialiser_) {
|
||||
return string_serialiser_->head() | 0x80;
|
||||
} else {
|
||||
return keyboard_input_;
|
||||
}
|
||||
}
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
Outputs::Speaker::LowpassSpeaker<Audio::Toggle> speaker_;
|
||||
Cycles cycles_since_audio_update_;
|
||||
|
||||
// MARK: - Cards
|
||||
std::array<std::unique_ptr<Apple::II::Card>, 7> cards_;
|
||||
Cycles cycles_since_card_update_;
|
||||
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, 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(Apple::II::Card *card) {
|
||||
return !card->get_select_constraints();
|
||||
}
|
||||
|
||||
void pick_card_messaging_group(Apple::II::Card *card) {
|
||||
const bool is_every_cycle = is_every_cycle_card(card);
|
||||
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);
|
||||
if(old_membership != undesired.end()) undesired.erase(old_membership);
|
||||
intended.push_back(card);
|
||||
}
|
||||
|
||||
void card_did_change_select_constraints(Apple::II::Card *card) override {
|
||||
pick_card_messaging_group(card);
|
||||
}
|
||||
|
||||
Apple::II::DiskIICard *diskii_card() {
|
||||
return dynamic_cast<Apple::II::DiskIICard *>(cards_[5].get());
|
||||
}
|
||||
|
||||
// MARK: - Memory Map.
|
||||
|
||||
/*
|
||||
The Apple II's paging mechanisms are byzantine to say the least. Painful is
|
||||
another appropriate adjective.
|
||||
|
||||
On a II and II+ there are five distinct zones of memory:
|
||||
|
||||
0000 to c000 : the main block of RAM
|
||||
c000 to d000 : the IO area, including card ROMs
|
||||
d000 to e000 : the low ROM area, which can alternatively contain either one of two 4kb blocks of RAM with a language card
|
||||
e000 onward : the rest of ROM, also potentially replaced with RAM by a language card
|
||||
|
||||
On a IIe with auxiliary memory the following orthogonal changes also need to be factored in:
|
||||
|
||||
0000 to 0200 : can be paged independently of the rest of RAM, other than part of the language card area which pages with it
|
||||
0400 to 0800 : the text screen, can be configured to write to auxiliary RAM
|
||||
2000 to 4000 : the graphics screen, which can be configured to write to auxiliary RAM
|
||||
c100 to d000 : can be used to page an additional 3.75kb of ROM, replacing the IO area
|
||||
c300 to c400 : can contain the same 256-byte segment of the ROM as if the whole IO area were switched, but while leaving cards visible in the rest
|
||||
c800 to d000 : can contain ROM separately from the region below c800
|
||||
|
||||
If dealt with as individual blocks in the inner loop, that would therefore imply mapping
|
||||
an address to one of 13 potential pageable zones. So I've gone reductive and surrendered
|
||||
to paging every 6502 page of memory independently. It makes the paging events more expensive,
|
||||
but hopefully more clear.
|
||||
*/
|
||||
uint8_t *read_pages_[256]; // each is a pointer to the 256-block of memory the CPU should read when accessing that page of memory
|
||||
uint8_t *write_pages_[256]; // as per read_pages_, but this is where the CPU should write. If a pointer is nullptr, don't write.
|
||||
void page(int start, int end, uint8_t *read, uint8_t *write) {
|
||||
for(int position = start; position < end; ++position) {
|
||||
read_pages_[position] = read;
|
||||
if(read) read += 256;
|
||||
|
||||
write_pages_[position] = write;
|
||||
if(write) write += 256;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The language card.
|
||||
struct {
|
||||
bool bank1 = false;
|
||||
bool read = false;
|
||||
bool pre_write = false;
|
||||
bool write = false;
|
||||
} language_card_;
|
||||
bool has_language_card_ = true;
|
||||
void set_language_card_paging() {
|
||||
uint8_t *const ram = alternative_zero_page_ ? aux_ram_ : ram_;
|
||||
uint8_t *const rom = is_iie() ? &rom_[3840] : rom_.data();
|
||||
|
||||
page(0xd0, 0xe0,
|
||||
language_card_.read ? &ram[language_card_.bank1 ? 0xd000 : 0xc000] : rom,
|
||||
language_card_.write ? nullptr : &ram[language_card_.bank1 ? 0xd000 : 0xc000]);
|
||||
|
||||
page(0xe0, 0x100,
|
||||
language_card_.read ? &ram[0xe000] : &rom[0x1000],
|
||||
language_card_.write ? nullptr : &ram[0xe000]);
|
||||
}
|
||||
|
||||
// MARK - The IIe's ROM controls.
|
||||
bool internal_CX_rom_ = false;
|
||||
bool slot_C3_rom_ = false;
|
||||
bool internal_c8_rom_ = false;
|
||||
|
||||
void set_card_paging() {
|
||||
page(0xc1, 0xc8, internal_CX_rom_ ? rom_.data() : nullptr, nullptr);
|
||||
|
||||
if(!internal_CX_rom_) {
|
||||
if(!slot_C3_rom_) read_pages_[0xc3] = &rom_[0xc300 - 0xc100];
|
||||
}
|
||||
|
||||
page(0xc8, 0xd0, (internal_CX_rom_ || internal_c8_rom_) ? &rom_[0xc800 - 0xc100] : nullptr, nullptr);
|
||||
}
|
||||
|
||||
// MARK - The IIe's auxiliary RAM controls.
|
||||
bool alternative_zero_page_ = false;
|
||||
void set_zero_page_paging() {
|
||||
if(alternative_zero_page_) {
|
||||
read_pages_[0] = aux_ram_;
|
||||
} else {
|
||||
read_pages_[0] = ram_;
|
||||
}
|
||||
read_pages_[1] = read_pages_[0] + 256;
|
||||
write_pages_[0] = read_pages_[0];
|
||||
write_pages_[1] = read_pages_[1];
|
||||
}
|
||||
|
||||
bool read_auxiliary_memory_ = false;
|
||||
bool write_auxiliary_memory_ = false;
|
||||
void set_main_paging() {
|
||||
page(0x02, 0xc0,
|
||||
read_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200],
|
||||
write_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200]);
|
||||
|
||||
if(video_.get_80_store()) {
|
||||
bool use_aux_ram = video_.get_page2();
|
||||
page(0x04, 0x08,
|
||||
use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400],
|
||||
use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400]);
|
||||
|
||||
if(video_.get_high_resolution()) {
|
||||
page(0x20, 0x40,
|
||||
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000],
|
||||
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK - typing
|
||||
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
|
||||
|
||||
// MARK - joysticks
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Horizontal),
|
||||
Input(Input::Vertical),
|
||||
|
||||
// The Apple II offers three buttons between two joysticks;
|
||||
// this emulator puts three buttons on each joystick and
|
||||
// combines them.
|
||||
Input(Input::Fire, 0),
|
||||
Input(Input::Fire, 1),
|
||||
Input(Input::Fire, 2),
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &input, float value) override {
|
||||
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical))
|
||||
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
|
||||
}
|
||||
|
||||
void did_set_input(const Input &input, bool value) override {
|
||||
if(input.type == Input::Type::Fire && input.info.control.index < 3) {
|
||||
buttons[input.info.control.index] = value;
|
||||
}
|
||||
}
|
||||
|
||||
bool buttons[3] = {false, false, false};
|
||||
float axes[2] = {0.5f, 0.5f};
|
||||
};
|
||||
|
||||
// On an Apple II, the programmer strobes 0xc070 and that causes each analogue input
|
||||
// to begin a charge and discharge cycle **if they are not already charging**.
|
||||
// The greater the analogue input, the faster they will charge and therefore the sooner
|
||||
// they will discharge.
|
||||
//
|
||||
// This emulator models that with analogue_charge_ being essentially the amount of time,
|
||||
// in charge threshold units, since 0xc070 was last strobed. But if any of the analogue
|
||||
// inputs were already partially charged then they gain a bias in analogue_biases_.
|
||||
//
|
||||
// It's a little indirect, but it means only having to increment the one value in the
|
||||
// main loop.
|
||||
float analogue_charge_ = 0.0f;
|
||||
float analogue_biases_[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
bool analogue_channel_is_discharged(size_t channel) {
|
||||
return (1.0f - static_cast<Joystick *>(joysticks_[channel >> 1].get())->axes[channel & 1]) < analogue_charge_ + analogue_biases_[channel];
|
||||
}
|
||||
|
||||
// The IIe has three keys that are wired directly to the same input as the joystick buttons.
|
||||
bool open_apple_is_pressed_ = false;
|
||||
bool closed_apple_is_pressed_ = false;
|
||||
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::AppleII::Target &target, const ROMMachine::ROMFetcher &rom_fetcher):
|
||||
m6502_(*this),
|
||||
video_bus_handler_(ram_, aux_ram_),
|
||||
video_(video_bus_handler_),
|
||||
audio_toggle_(audio_queue_),
|
||||
speaker_(audio_toggle_) {
|
||||
// The system's master clock rate.
|
||||
const float master_clock = 14318180.0;
|
||||
|
||||
// This is where things get slightly convoluted: establish the machine as having a clock rate
|
||||
// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
|
||||
// the master clock rate divided by 14 because every 65th cycle is extended by one seventh.
|
||||
set_clock_rate((master_clock / 14.0) * 65.0 / (65.0 + 1.0 / 7.0));
|
||||
|
||||
// The speaker, however, should think it is clocked at half the master clock, per a general
|
||||
// decision to sample it at seven times the CPU clock (plus stretches).
|
||||
speaker_.set_input_rate(static_cast<float>(master_clock / (2.0 * static_cast<float>(audio_divider))));
|
||||
|
||||
// Apply a 6Khz low-pass filter. This was picked by ear and by an attempt to understand the
|
||||
// Apple II schematic but, well, I don't claim much insight on the latter. This is definitely
|
||||
// something to review in the future.
|
||||
speaker_.set_high_frequency_cutoff(6000);
|
||||
|
||||
// Also, start with randomised memory contents.
|
||||
Memory::Fuzz(ram_, sizeof(ram_));
|
||||
Memory::Fuzz(aux_ram_, sizeof(aux_ram_));
|
||||
|
||||
// Add a couple of joysticks.
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
|
||||
// Pick the required ROMs.
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
const std::string machine_name = "AppleII";
|
||||
std::vector<ROMMachine::ROM> rom_descriptions;
|
||||
size_t rom_size = 12*1024;
|
||||
switch(target.model) {
|
||||
default:
|
||||
rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
|
||||
rom_descriptions.emplace_back(machine_name, "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588);
|
||||
break;
|
||||
case Target::Model::IIplus:
|
||||
rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26);
|
||||
break;
|
||||
case Target::Model::IIe:
|
||||
rom_size += 3840;
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1);
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18d);
|
||||
break;
|
||||
case Target::Model::EnhancedIIe:
|
||||
rom_size += 3840;
|
||||
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d);
|
||||
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942);
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
rom_ = std::move(*roms[1]);
|
||||
if(rom_.size() > rom_size) {
|
||||
rom_.erase(rom_.begin(), rom_.end() - static_cast<off_t>(rom_size));
|
||||
}
|
||||
|
||||
video_.set_character_rom(*roms[0]);
|
||||
|
||||
// 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();
|
||||
set_zero_page_paging();
|
||||
|
||||
// Set the whole card area to initially backed by nothing.
|
||||
page(0xc0, 0xd0, nullptr, nullptr);
|
||||
|
||||
// Set proper values for the language card/ROM area.
|
||||
set_language_card_paging();
|
||||
|
||||
insert_media(target.media);
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
video_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// Sets the type of display.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
video_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
forceinline Cycles perform_bus_operation(const CPU::MOS6502::BusOperation operation, const uint16_t address, uint8_t *const value) {
|
||||
++ cycles_since_video_update_;
|
||||
++ cycles_since_card_update_;
|
||||
cycles_since_audio_update_ += Cycles(7);
|
||||
|
||||
// The Apple II has a slightly weird timing pattern: every 65th CPU cycle is stretched
|
||||
// by an extra 1/7th. That's because one cycle lasts 3.5 NTSC colour clocks, so after
|
||||
// 65 cycles a full line of 227.5 colour clocks have passed. But the high-rate binary
|
||||
// signal approximation that produces colour needs to be in phase, so a stretch of exactly
|
||||
// 0.5 further colour cycles is added. The video class handles that implicitly, but it
|
||||
// needs to be accumulated here for the audio.
|
||||
cycles_into_current_line_ = (cycles_into_current_line_ + 1) % 65;
|
||||
const bool is_stretched_cycle = !cycles_into_current_line_;
|
||||
if(is_stretched_cycle) {
|
||||
++ cycles_since_audio_update_;
|
||||
++ stretched_cycles_since_card_update_;
|
||||
}
|
||||
|
||||
bool has_updated_cards = false;
|
||||
if(read_pages_[address >> 8]) {
|
||||
if(isReadOperation(operation)) *value = read_pages_[address >> 8][address & 0xff];
|
||||
else {
|
||||
if(address >= 0x200 && address < 0x6000) update_video();
|
||||
if(write_pages_[address >> 8]) write_pages_[address >> 8][address & 0xff] = *value;
|
||||
}
|
||||
|
||||
if(is_iie() && address >= 0xc300 && address < 0xd000) {
|
||||
bool internal_c8_rom = internal_c8_rom_;
|
||||
internal_c8_rom |= ((address >> 8) == 0xc3) && !slot_C3_rom_;
|
||||
internal_c8_rom &= (address != 0xcfff);
|
||||
if(internal_c8_rom != internal_c8_rom_) {
|
||||
internal_c8_rom_ = internal_c8_rom;
|
||||
set_card_paging();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Assume a vapour read unless it turns out otherwise; this is a little
|
||||
// wasteful but works for now.
|
||||
//
|
||||
// Longer version: like many other machines, when the Apple II reads from
|
||||
// an address at which no hardware loads the data bus, through a process of
|
||||
// practical analogue effects it'll end up receiving whatever was last on
|
||||
// the bus. Which will always be whatever the video circuit fetched because
|
||||
// that fetches in between every instruction.
|
||||
//
|
||||
// So this code assumes that'll happen unless it later determines that it
|
||||
// doesn't. The call into the video isn't free because it's a just-in-time
|
||||
// actor, but this will actually be the result most of the time so it's not
|
||||
// too terrible.
|
||||
if(isReadOperation(operation) && address != 0xc000) {
|
||||
*value = video_.get_last_read_value(cycles_since_video_update_);
|
||||
}
|
||||
|
||||
switch(address) {
|
||||
default:
|
||||
if(isReadOperation(operation)) {
|
||||
// Read-only switches.
|
||||
switch(address) {
|
||||
default: break;
|
||||
|
||||
case 0xc000:
|
||||
*value = get_keyboard_input();
|
||||
break;
|
||||
case 0xc001: case 0xc002: case 0xc003: case 0xc004: case 0xc005: case 0xc006: case 0xc007:
|
||||
case 0xc008: case 0xc009: case 0xc00a: case 0xc00b: case 0xc00c: case 0xc00d: case 0xc00e: case 0xc00f:
|
||||
*value = (*value & 0x80) | (get_keyboard_input() & 0x7f);
|
||||
break;
|
||||
|
||||
case 0xc061: // Switch input 0.
|
||||
*value &= 0x7f;
|
||||
if(
|
||||
static_cast<Joystick *>(joysticks_[0].get())->buttons[0] || static_cast<Joystick *>(joysticks_[1].get())->buttons[2] ||
|
||||
(is_iie() && open_apple_is_pressed_)
|
||||
)
|
||||
*value |= 0x80;
|
||||
break;
|
||||
case 0xc062: // Switch input 1.
|
||||
*value &= 0x7f;
|
||||
if(
|
||||
static_cast<Joystick *>(joysticks_[0].get())->buttons[1] || static_cast<Joystick *>(joysticks_[1].get())->buttons[1] ||
|
||||
(is_iie() && closed_apple_is_pressed_)
|
||||
)
|
||||
*value |= 0x80;
|
||||
break;
|
||||
case 0xc063: // Switch input 2.
|
||||
*value &= 0x7f;
|
||||
if(static_cast<Joystick *>(joysticks_[0].get())->buttons[2] || static_cast<Joystick *>(joysticks_[1].get())->buttons[0])
|
||||
*value |= 0x80;
|
||||
break;
|
||||
|
||||
case 0xc064: // Analogue input 0.
|
||||
case 0xc065: // Analogue input 1.
|
||||
case 0xc066: // Analogue input 2.
|
||||
case 0xc067: { // Analogue input 3.
|
||||
const size_t input = address - 0xc064;
|
||||
*value &= 0x7f;
|
||||
if(!analogue_channel_is_discharged(input)) {
|
||||
*value |= 0x80;
|
||||
}
|
||||
} break;
|
||||
|
||||
// The IIe-only state reads follow...
|
||||
#define IIeSwitchRead(s) *value = get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00);
|
||||
case 0xc011: IIeSwitchRead(language_card_.bank1); break;
|
||||
case 0xc012: IIeSwitchRead(language_card_.read); break;
|
||||
case 0xc013: IIeSwitchRead(read_auxiliary_memory_); break;
|
||||
case 0xc014: IIeSwitchRead(write_auxiliary_memory_); break;
|
||||
case 0xc015: IIeSwitchRead(internal_CX_rom_); break;
|
||||
case 0xc016: IIeSwitchRead(alternative_zero_page_); break;
|
||||
case 0xc017: IIeSwitchRead(slot_C3_rom_); break;
|
||||
case 0xc018: IIeSwitchRead(video_.get_80_store()); break;
|
||||
case 0xc019: IIeSwitchRead(video_.get_is_vertical_blank(cycles_since_video_update_)); break;
|
||||
case 0xc01a: IIeSwitchRead(video_.get_text()); break;
|
||||
case 0xc01b: IIeSwitchRead(video_.get_mixed()); break;
|
||||
case 0xc01c: IIeSwitchRead(video_.get_page2()); break;
|
||||
case 0xc01d: IIeSwitchRead(video_.get_high_resolution()); break;
|
||||
case 0xc01e: IIeSwitchRead(video_.get_alternative_character_set()); break;
|
||||
case 0xc01f: IIeSwitchRead(video_.get_80_columns()); break;
|
||||
#undef IIeSwitchRead
|
||||
|
||||
case 0xc07f:
|
||||
if(is_iie()) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Write-only switches. All IIe as currently implemented.
|
||||
if(is_iie()) {
|
||||
switch(address) {
|
||||
default: break;
|
||||
|
||||
case 0xc000:
|
||||
case 0xc001:
|
||||
update_video();
|
||||
video_.set_80_store(!!(address&1));
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc002:
|
||||
case 0xc003:
|
||||
read_auxiliary_memory_ = !!(address&1);
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc004:
|
||||
case 0xc005:
|
||||
write_auxiliary_memory_ = !!(address&1);
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc006:
|
||||
case 0xc007:
|
||||
internal_CX_rom_ = !!(address&1);
|
||||
set_card_paging();
|
||||
break;
|
||||
|
||||
case 0xc008:
|
||||
case 0xc009:
|
||||
// The alternative zero page setting affects both bank 0 and any RAM
|
||||
// that's paged as though it were on a language card.
|
||||
alternative_zero_page_ = !!(address&1);
|
||||
set_zero_page_paging();
|
||||
set_language_card_paging();
|
||||
break;
|
||||
|
||||
case 0xc00a:
|
||||
case 0xc00b:
|
||||
slot_C3_rom_ = !!(address&1);
|
||||
set_card_paging();
|
||||
break;
|
||||
|
||||
case 0xc00c:
|
||||
case 0xc00d:
|
||||
update_video();
|
||||
video_.set_80_columns(!!(address&1));
|
||||
break;
|
||||
|
||||
case 0xc00e:
|
||||
case 0xc00f:
|
||||
update_video();
|
||||
video_.set_alternative_character_set(!!(address&1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc070: { // Permit analogue inputs that are currently discharged to begin a charge cycle.
|
||||
// Ensure those that were still charging retain that state.
|
||||
for(size_t c = 0; c < 4; ++c) {
|
||||
if(analogue_channel_is_discharged(c)) {
|
||||
analogue_biases_[c] = 0.0f;
|
||||
} else {
|
||||
analogue_biases_[c] += analogue_charge_;
|
||||
}
|
||||
}
|
||||
analogue_charge_ = 0.0f;
|
||||
} break;
|
||||
|
||||
/* Switches triggered by reading or writing. */
|
||||
case 0xc050:
|
||||
case 0xc051:
|
||||
update_video();
|
||||
video_.set_text(!!(address&1));
|
||||
break;
|
||||
case 0xc052: update_video(); video_.set_mixed(false); break;
|
||||
case 0xc053: update_video(); video_.set_mixed(true); break;
|
||||
case 0xc054:
|
||||
case 0xc055:
|
||||
update_video();
|
||||
video_.set_page2(!!(address&1));
|
||||
set_main_paging();
|
||||
break;
|
||||
case 0xc056:
|
||||
case 0xc057:
|
||||
update_video();
|
||||
video_.set_high_resolution(!!(address&1));
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc05e:
|
||||
case 0xc05f:
|
||||
if(is_iie()) {
|
||||
update_video();
|
||||
video_.set_annunciator_3(!(address&1));
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc010:
|
||||
keyboard_input_ &= 0x7f;
|
||||
if(string_serialiser_) {
|
||||
if(!string_serialiser_->advance())
|
||||
string_serialiser_.reset();
|
||||
}
|
||||
|
||||
// On the IIe, reading C010 returns additional key info.
|
||||
if(is_iie() && isReadOperation(operation)) {
|
||||
*value = (key_is_down_ ? 0x80 : 0x00) | (keyboard_input_ & 0x7f);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc030: case 0xc031: case 0xc032: case 0xc033: case 0xc034: case 0xc035: case 0xc036: case 0xc037:
|
||||
case 0xc038: case 0xc039: case 0xc03a: case 0xc03b: case 0xc03c: case 0xc03d: case 0xc03e: case 0xc03f:
|
||||
update_audio();
|
||||
audio_toggle_.set_output(!audio_toggle_.get_output());
|
||||
break;
|
||||
|
||||
case 0xc080: case 0xc084: case 0xc088: case 0xc08c:
|
||||
case 0xc081: case 0xc085: case 0xc089: case 0xc08d:
|
||||
case 0xc082: case 0xc086: case 0xc08a: case 0xc08e:
|
||||
case 0xc083: case 0xc087: case 0xc08b: case 0xc08f:
|
||||
// Quotes below taken from Understanding the Apple II, p. 5-28 and 5-29.
|
||||
|
||||
// "A3 controls the 4K bank selection"
|
||||
language_card_.bank1 = (address&8);
|
||||
|
||||
// "Access to $C080, $C083, $C084, $0087, $C088, $C08B, $C08C, or $C08F sets the READ ENABLE flip-flop"
|
||||
// (other accesses reset it)
|
||||
language_card_.read = !(((address&2) >> 1) ^ (address&1));
|
||||
|
||||
// "The WRITE ENABLE' flip-flop is reset by an odd read access to the $C08X range when the PRE-WRITE flip-flop is set."
|
||||
if(language_card_.pre_write && isReadOperation(operation) && (address&1)) language_card_.write = false;
|
||||
|
||||
// "[The WRITE ENABLE' flip-flop] is set by an even access in the $C08X range."
|
||||
if(!(address&1)) language_card_.write = true;
|
||||
|
||||
// ("Any other type of access causes the WRITE ENABLE' flip-flop to hold its current state.")
|
||||
|
||||
// "The PRE-WRITE flip-flop is set by an odd read access in the $C08X range. It is reset by an even access or a write access."
|
||||
language_card_.pre_write = isReadOperation(operation) ? (address&1) : false;
|
||||
|
||||
// Apply whatever the net effect of all that is to the memory map.
|
||||
set_language_card_paging();
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
Communication with cards follows.
|
||||
*/
|
||||
|
||||
if(!read_pages_[address >> 8] && address >= 0xc090 && address < 0xc800) {
|
||||
// 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;
|
||||
Apple::II::Card::Select select = Apple::II::Card::None;
|
||||
|
||||
if(address >= 0xc100) {
|
||||
/*
|
||||
Decode the area conventionally used by cards for ROMs:
|
||||
0xCn00 to 0xCnff: card n.
|
||||
*/
|
||||
card_number = (address - 0xc100) >> 8;
|
||||
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 = 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);
|
||||
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);
|
||||
}
|
||||
|
||||
// Update all the every-cycle cards regardless, but send them a ::None select if they're
|
||||
// not the one actually selected.
|
||||
for(const auto &card: every_cycle_cards_) {
|
||||
card->run_for(Cycles(1), is_stretched_cycle);
|
||||
card->perform_bus_operation(
|
||||
(card == target) ? select : Apple::II::Card::None,
|
||||
is_read, address, value);
|
||||
}
|
||||
has_updated_cards = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!has_updated_cards && !every_cycle_cards_.empty()) {
|
||||
// Update all every-cycle cards and give them the cycle.
|
||||
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(Apple::II::Card::None, is_read, address, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Update analogue charge level.
|
||||
analogue_charge_ = std::min(analogue_charge_ + 1.0f / 2820.0f, 1.1f);
|
||||
|
||||
return Cycles(1);
|
||||
}
|
||||
|
||||
void flush() {
|
||||
update_video();
|
||||
update_audio();
|
||||
update_just_in_time_cards();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void reset_all_keys() override {
|
||||
open_apple_is_pressed_ = closed_apple_is_pressed_ = key_is_down_ = false;
|
||||
}
|
||||
|
||||
void set_key_pressed(Key key, char value, bool is_pressed) override {
|
||||
switch(key) {
|
||||
default: break;
|
||||
case Key::F12:
|
||||
m6502_.set_reset_line(is_pressed);
|
||||
return;
|
||||
case Key::LeftOption:
|
||||
open_apple_is_pressed_ = is_pressed;
|
||||
return;
|
||||
case Key::RightOption:
|
||||
closed_apple_is_pressed_ = is_pressed;
|
||||
return;
|
||||
}
|
||||
|
||||
// If no ASCII value is supplied, look for a few special cases.
|
||||
if(!value) {
|
||||
switch(key) {
|
||||
case Key::Left: value = 0x08; break;
|
||||
case Key::Right: value = 0x15; break;
|
||||
case Key::Down: value = 0x0a; break;
|
||||
case Key::Up: value = 0x0b; break;
|
||||
case Key::BackSpace: value = 0x7f; break;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prior to the IIe, the keyboard could produce uppercase only.
|
||||
if(!is_iie()) value = static_cast<char>(toupper(value));
|
||||
|
||||
if(is_pressed) {
|
||||
keyboard_input_ = static_cast<uint8_t>(value | 0x80);
|
||||
key_is_down_ = true;
|
||||
} else {
|
||||
if((keyboard_input_ & 0x7f) == value) {
|
||||
key_is_down_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Inputs::Keyboard &get_keyboard() override {
|
||||
return *this;
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) override {
|
||||
string_serialiser_.reset(new Utility::StringSerialiser(string, true));
|
||||
}
|
||||
|
||||
// MARK:: Configuration options.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
return Apple::II::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 {
|
||||
return get_accurate_selections();
|
||||
}
|
||||
|
||||
// MARK: MediaTarget
|
||||
bool insert_media(const Analyser::Static::Media &media) override {
|
||||
if(!media.disks.empty()) {
|
||||
auto diskii = diskii_card();
|
||||
if(diskii) diskii->set_disk(media.disks[0], 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: Activity::Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
for(const auto &card: cards_) {
|
||||
if(card) card->set_activity_observer(observer);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: JoystickMachine
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
return joysticks_;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
using namespace Apple::II;
|
||||
|
||||
Machine *Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
const Target *const appleii_target = dynamic_cast<const Target *>(target);
|
||||
switch(appleii_target->model) {
|
||||
default: return nullptr;
|
||||
case Target::Model::II: return new ConcreteMachine<Target::Model::II>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::IIplus: return new ConcreteMachine<Target::Model::IIplus>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::IIe: return new ConcreteMachine<Target::Model::IIe>(*appleii_target, rom_fetcher);
|
||||
case Target::Model::EnhancedIIe: return new ConcreteMachine<Target::Model::EnhancedIIe>(*appleii_target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
@@ -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
|
||||
@@ -39,6 +40,7 @@ namespace AppleII {
|
||||
*/
|
||||
class Card {
|
||||
public:
|
||||
virtual ~Card() {}
|
||||
enum Select: int {
|
||||
None = 0, // No select line is active
|
||||
IO = 1 << 0, // IO select is active
|
||||
@@ -108,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 */
|
||||
340
Machines/Apple/AppleII/Video.cpp
Normal file
340
Machines/Apple/AppleII/Video.cpp
Normal file
@@ -0,0 +1,340 @@
|
||||
//
|
||||
// Video.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/04/2018.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Video.hpp"
|
||||
|
||||
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),
|
||||
is_iie_(is_iie),
|
||||
deferrer_(std::move(target)) {
|
||||
|
||||
// Show only the centre 75% of the TV frame.
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::CompositeColour);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.118f, 0.122f, 0.77f, 0.77f));
|
||||
|
||||
// TODO: there seems to be some sort of bug whereby switching modes can cause
|
||||
// a signal discontinuity that knocks phase out of whack. So it isn't safe to
|
||||
// use default_colour_bursts elsewhere, though it otherwise should be. If/when
|
||||
// it is, start doing so and return to setting the immediate phase up here.
|
||||
// crt_.set_immediate_default_phase(0.5f);
|
||||
|
||||
character_zones[0].xor_mask = 0;
|
||||
character_zones[0].address_mask = 0x3f;
|
||||
character_zones[1].xor_mask = 0;
|
||||
character_zones[1].address_mask = 0x3f;
|
||||
character_zones[2].xor_mask = 0;
|
||||
character_zones[2].address_mask = 0x3f;
|
||||
character_zones[3].xor_mask = 0;
|
||||
character_zones[3].address_mask = 0x3f;
|
||||
|
||||
if(is_iie) {
|
||||
character_zones[0].xor_mask =
|
||||
character_zones[2].xor_mask =
|
||||
character_zones[3].xor_mask = 0xff;
|
||||
character_zones[2].address_mask =
|
||||
character_zones[3].address_mask = 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
/*
|
||||
Rote setters and getters.
|
||||
*/
|
||||
void VideoBase::set_alternative_character_set(bool alternative_character_set) {
|
||||
set_alternative_character_set_ = alternative_character_set;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
alternative_character_set_ = alternative_character_set;
|
||||
if(alternative_character_set) {
|
||||
character_zones[1].address_mask = 0xff;
|
||||
character_zones[1].xor_mask = 0;
|
||||
} else {
|
||||
character_zones[1].address_mask = 0x3f;
|
||||
character_zones[1].xor_mask = flash_mask();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_alternative_character_set() {
|
||||
return set_alternative_character_set_;
|
||||
}
|
||||
|
||||
void VideoBase::set_80_columns(bool columns_80) {
|
||||
set_columns_80_ = columns_80;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
columns_80_ = columns_80;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_80_columns() {
|
||||
return set_columns_80_;
|
||||
}
|
||||
|
||||
void VideoBase::set_80_store(bool store_80) {
|
||||
set_store_80_ = store_80_ = store_80;
|
||||
}
|
||||
|
||||
bool VideoBase::get_80_store() {
|
||||
return set_store_80_;
|
||||
}
|
||||
|
||||
void VideoBase::set_page2(bool page2) {
|
||||
set_page2_ = page2_ = page2;
|
||||
}
|
||||
|
||||
bool VideoBase::get_page2() {
|
||||
return set_page2_;
|
||||
}
|
||||
|
||||
void VideoBase::set_text(bool text) {
|
||||
set_text_ = text;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
text_ = text;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_text() {
|
||||
return set_text_;
|
||||
}
|
||||
|
||||
void VideoBase::set_mixed(bool mixed) {
|
||||
set_mixed_ = mixed;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
mixed_ = mixed;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_mixed() {
|
||||
return set_mixed_;
|
||||
}
|
||||
|
||||
void VideoBase::set_high_resolution(bool high_resolution) {
|
||||
set_high_resolution_ = high_resolution;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
high_resolution_ = high_resolution;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_high_resolution() {
|
||||
return set_high_resolution_;
|
||||
}
|
||||
|
||||
void VideoBase::set_annunciator_3(bool annunciator_3) {
|
||||
set_annunciator_3_ = annunciator_3;
|
||||
deferrer_.defer(Cycles(2), [=] {
|
||||
annunciator_3_ = annunciator_3;
|
||||
high_resolution_mask_ = annunciator_3_ ? 0x7f : 0xff;
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_annunciator_3() {
|
||||
return set_annunciator_3_;
|
||||
}
|
||||
|
||||
void VideoBase::set_character_rom(const std::vector<uint8_t> &character_rom) {
|
||||
character_rom_ = character_rom;
|
||||
|
||||
// Flip all character contents based on the second line of the $ graphic.
|
||||
if(character_rom_[0x121] == 0x3c || character_rom_[0x122] == 0x3c) {
|
||||
for(auto &graphic : character_rom_) {
|
||||
graphic =
|
||||
((graphic & 0x01) ? 0x40 : 0x00) |
|
||||
((graphic & 0x02) ? 0x20 : 0x00) |
|
||||
((graphic & 0x04) ? 0x10 : 0x00) |
|
||||
((graphic & 0x08) ? 0x08 : 0x00) |
|
||||
((graphic & 0x10) ? 0x04 : 0x00) |
|
||||
((graphic & 0x20) ? 0x02 : 0x00) |
|
||||
((graphic & 0x40) ? 0x01 : 0x00);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_text(uint8_t *target, const uint8_t *const source, size_t length, size_t pixel_row) const {
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
const int character = source[c] & character_zones[source[c] >> 6].address_mask;
|
||||
const uint8_t xor_mask = character_zones[source[c] >> 6].xor_mask;
|
||||
const std::size_t character_address = static_cast<std::size_t>(character << 3) + pixel_row;
|
||||
const uint8_t character_pattern = character_rom_[character_address] ^ xor_mask;
|
||||
|
||||
// The character ROM is output MSB to LSB rather than LSB to MSB.
|
||||
target[0] = target[1] = character_pattern & 0x40;
|
||||
target[2] = target[3] = character_pattern & 0x20;
|
||||
target[4] = target[5] = character_pattern & 0x10;
|
||||
target[6] = target[7] = character_pattern & 0x08;
|
||||
target[8] = target[9] = character_pattern & 0x04;
|
||||
target[10] = target[11] = character_pattern & 0x02;
|
||||
target[12] = target[13] = character_pattern & 0x01;
|
||||
graphics_carry_ = character_pattern & 0x01;
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_double_text(uint8_t *target, const uint8_t *const source, const uint8_t *const auxiliary_source, size_t length, size_t pixel_row) const {
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
const std::size_t character_addresses[2] = {
|
||||
static_cast<std::size_t>(
|
||||
(auxiliary_source[c] & character_zones[auxiliary_source[c] >> 6].address_mask) << 3
|
||||
) + pixel_row,
|
||||
static_cast<std::size_t>(
|
||||
(source[c] & character_zones[source[c] >> 6].address_mask) << 3
|
||||
) + pixel_row
|
||||
};
|
||||
|
||||
const uint8_t character_patterns[2] = {
|
||||
static_cast<uint8_t>(
|
||||
character_rom_[character_addresses[0]] ^ character_zones[auxiliary_source[c] >> 6].xor_mask
|
||||
),
|
||||
static_cast<uint8_t>(
|
||||
character_rom_[character_addresses[1]] ^ character_zones[source[c] >> 6].xor_mask
|
||||
)
|
||||
};
|
||||
|
||||
// The character ROM is output MSB to LSB rather than LSB to MSB.
|
||||
target[0] = character_patterns[0] & 0x40;
|
||||
target[1] = character_patterns[0] & 0x20;
|
||||
target[2] = character_patterns[0] & 0x10;
|
||||
target[3] = character_patterns[0] & 0x08;
|
||||
target[4] = character_patterns[0] & 0x04;
|
||||
target[5] = character_patterns[0] & 0x02;
|
||||
target[6] = character_patterns[0] & 0x01;
|
||||
target[7] = character_patterns[1] & 0x40;
|
||||
target[8] = character_patterns[1] & 0x20;
|
||||
target[9] = character_patterns[1] & 0x10;
|
||||
target[10] = character_patterns[1] & 0x08;
|
||||
target[11] = character_patterns[1] & 0x04;
|
||||
target[12] = character_patterns[1] & 0x02;
|
||||
target[13] = character_patterns[1] & 0x01;
|
||||
graphics_carry_ = character_patterns[1] & 0x01;
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_low_resolution(uint8_t *target, const uint8_t *const source, size_t length, int column, int row) const {
|
||||
const int row_shift = row&4;
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
// Low-resolution graphics mode shifts the colour code on a loop, but has to account for whether this
|
||||
// 14-sample output window is starting at the beginning of a colour cycle or halfway through.
|
||||
if((column + static_cast<int>(c))&1) {
|
||||
target[0] = target[4] = target[8] = target[12] = (source[c] >> row_shift) & 4;
|
||||
target[1] = target[5] = target[9] = target[13] = (source[c] >> row_shift) & 8;
|
||||
target[2] = target[6] = target[10] = (source[c] >> row_shift) & 1;
|
||||
target[3] = target[7] = target[11] = (source[c] >> row_shift) & 2;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 8;
|
||||
} else {
|
||||
target[0] = target[4] = target[8] = target[12] = (source[c] >> row_shift) & 1;
|
||||
target[1] = target[5] = target[9] = target[13] = (source[c] >> row_shift) & 2;
|
||||
target[2] = target[6] = target[10] = (source[c] >> row_shift) & 4;
|
||||
target[3] = target[7] = target[11] = (source[c] >> row_shift) & 8;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 2;
|
||||
}
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_fat_low_resolution(uint8_t *target, const uint8_t *const source, size_t length, int column, int row) const {
|
||||
const int row_shift = row&4;
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
// Fat low-resolution mode appears not to do anything to try to make odd and
|
||||
// even columns compatible.
|
||||
target[0] = target[1] = target[8] = target[9] = (source[c] >> row_shift) & 1;
|
||||
target[2] = target[3] = target[10] = target[11] = (source[c] >> row_shift) & 2;
|
||||
target[4] = target[5] = target[12] = target[13] = (source[c] >> row_shift) & 4;
|
||||
target[6] = target[7] = (source[c] >> row_shift) & 8;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 4;
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_double_low_resolution(uint8_t *target, const uint8_t *const source, const uint8_t *const auxiliary_source, size_t length, int column, int row) const {
|
||||
const int row_shift = row&4;
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
if((column + static_cast<int>(c))&1) {
|
||||
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 2;
|
||||
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 4;
|
||||
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 8;
|
||||
target[3] = (auxiliary_source[c] >> row_shift) & 1;
|
||||
|
||||
target[8] = target[12] = (source[c] >> row_shift) & 4;
|
||||
target[9] = target[13] = (source[c] >> row_shift) & 8;
|
||||
target[10] = (source[c] >> row_shift) & 1;
|
||||
target[7] = target[11] = (source[c] >> row_shift) & 2;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 8;
|
||||
} else {
|
||||
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 8;
|
||||
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 1;
|
||||
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 2;
|
||||
target[3] = (auxiliary_source[c] >> row_shift) & 4;
|
||||
|
||||
target[8] = target[12] = (source[c] >> row_shift) & 1;
|
||||
target[9] = target[13] = (source[c] >> row_shift) & 2;
|
||||
target[10] = (source[c] >> row_shift) & 4;
|
||||
target[7] = target[11] = (source[c] >> row_shift) & 8;
|
||||
graphics_carry_ = (source[c] >> row_shift) & 2;
|
||||
}
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_high_resolution(uint8_t *target, const uint8_t *const source, size_t length) const {
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
// High resolution graphics shift out LSB to MSB, optionally with a delay of half a pixel.
|
||||
// If there is a delay, the previous output level is held to bridge the gap.
|
||||
// Delays may be ignored on a IIe if Annunciator 3 is set; that's the state that
|
||||
// high_resolution_mask_ models.
|
||||
if(source[c] & high_resolution_mask_ & 0x80) {
|
||||
target[0] = graphics_carry_;
|
||||
target[1] = target[2] = source[c] & 0x01;
|
||||
target[3] = target[4] = source[c] & 0x02;
|
||||
target[5] = target[6] = source[c] & 0x04;
|
||||
target[7] = target[8] = source[c] & 0x08;
|
||||
target[9] = target[10] = source[c] & 0x10;
|
||||
target[11] = target[12] = source[c] & 0x20;
|
||||
target[13] = source[c] & 0x40;
|
||||
} else {
|
||||
target[0] = target[1] = source[c] & 0x01;
|
||||
target[2] = target[3] = source[c] & 0x02;
|
||||
target[4] = target[5] = source[c] & 0x04;
|
||||
target[6] = target[7] = source[c] & 0x08;
|
||||
target[8] = target[9] = source[c] & 0x10;
|
||||
target[10] = target[11] = source[c] & 0x20;
|
||||
target[12] = target[13] = source[c] & 0x40;
|
||||
}
|
||||
graphics_carry_ = source[c] & 0x40;
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::output_double_high_resolution(uint8_t *target, const uint8_t *const source, const uint8_t *const auxiliary_source, size_t length) const {
|
||||
for(size_t c = 0; c < length; ++c) {
|
||||
target[0] = auxiliary_source[c] & 0x01;
|
||||
target[1] = auxiliary_source[c] & 0x02;
|
||||
target[2] = auxiliary_source[c] & 0x04;
|
||||
target[3] = auxiliary_source[c] & 0x08;
|
||||
target[4] = auxiliary_source[c] & 0x10;
|
||||
target[5] = auxiliary_source[c] & 0x20;
|
||||
target[6] = auxiliary_source[c] & 0x40;
|
||||
target[7] = source[c] & 0x01;
|
||||
target[8] = source[c] & 0x02;
|
||||
target[9] = source[c] & 0x04;
|
||||
target[10] = source[c] & 0x08;
|
||||
target[11] = source[c] & 0x10;
|
||||
target[12] = source[c] & 0x20;
|
||||
target[13] = source[c] & 0x40;
|
||||
|
||||
graphics_carry_ = auxiliary_source[c] & 0x40;
|
||||
target += 14;
|
||||
}
|
||||
}
|
||||
608
Machines/Apple/AppleII/Video.hpp
Normal file
608
Machines/Apple/AppleII/Video.hpp
Normal file
@@ -0,0 +1,608 @@
|
||||
//
|
||||
// Video.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/04/2018.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Video_hpp
|
||||
#define Video_hpp
|
||||
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../ClockReceiver/DeferredQueue.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace Apple {
|
||||
namespace II {
|
||||
namespace Video {
|
||||
|
||||
class BusHandler {
|
||||
public:
|
||||
/*!
|
||||
Requests fetching of the @c count bytes starting from @c address.
|
||||
|
||||
The handler should write the values from base memory to @c base_target, and those
|
||||
from auxiliary memory to @c auxiliary_target. If the machine has no axiliary memory,
|
||||
it needn't write anything to auxiliary_target.
|
||||
*/
|
||||
void perform_read(uint16_t address, size_t count, uint8_t *base_target, uint8_t *auxiliary_target) {
|
||||
}
|
||||
};
|
||||
|
||||
class VideoBase {
|
||||
public:
|
||||
VideoBase(bool is_iie, std::function<void(Cycles)> &&target);
|
||||
|
||||
/// Sets the scan target.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Sets the type of output.
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
/*
|
||||
Descriptions for the setters below are taken verbatim from
|
||||
the Apple IIe Technical Reference. Addresses are the conventional
|
||||
locations within the Apple II memory map. Only those which affect
|
||||
video output are implemented here.
|
||||
|
||||
Those registers which don't exist on a II/II+ are marked.
|
||||
*/
|
||||
|
||||
/*!
|
||||
Setter for ALTCHAR ($C00E/$C00F; triggers on write only):
|
||||
|
||||
* Off: display text using primary character set.
|
||||
* On: display text using alternate character set.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_alternative_character_set(bool);
|
||||
bool get_alternative_character_set();
|
||||
|
||||
/*!
|
||||
Setter for 80COL ($C00C/$C00D; triggers on write only).
|
||||
|
||||
* Off: display 40 columns.
|
||||
* On: display 80 columns.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_80_columns(bool);
|
||||
bool get_80_columns();
|
||||
|
||||
/*!
|
||||
Setter for 80STORE ($C000/$C001; triggers on write only).
|
||||
|
||||
* Off: cause PAGE2 to select auxiliary RAM.
|
||||
* On: cause PAGE2 to switch main RAM areas.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_80_store(bool);
|
||||
bool get_80_store();
|
||||
|
||||
/*!
|
||||
Setter for PAGE2 ($C054/$C055; triggers on read or write).
|
||||
|
||||
* Off: select Page 1.
|
||||
* On: select Page 2 or, if 80STORE on, Page 1 in auxiliary memory.
|
||||
|
||||
80STORE doesn't exist on a II/II+; therefore this always selects
|
||||
either Page 1 or Page 2 on those machines.
|
||||
*/
|
||||
void set_page2(bool);
|
||||
bool get_page2();
|
||||
|
||||
/*!
|
||||
Setter for TEXT ($C050/$C051; triggers on read or write).
|
||||
|
||||
* Off: display graphics or, if MIXED on, mixed.
|
||||
* On: display text.
|
||||
*/
|
||||
void set_text(bool);
|
||||
bool get_text();
|
||||
|
||||
/*!
|
||||
Setter for MIXED ($C052/$C053; triggers on read or write).
|
||||
|
||||
* Off: display only text or only graphics.
|
||||
* On: if TEXT off, display text and graphics.
|
||||
*/
|
||||
void set_mixed(bool);
|
||||
bool get_mixed();
|
||||
|
||||
/*!
|
||||
Setter for HIRES ($C056/$C057; triggers on read or write).
|
||||
|
||||
* Off: if TEXT off, display low-resolution graphics.
|
||||
* On: if TEXT off, display high-resolution or, if DHIRES on, double high-resolution graphics.
|
||||
|
||||
DHIRES doesn't exist on a II/II+; therefore this always selects
|
||||
either high- or low-resolution graphics on those machines.
|
||||
|
||||
Despite Apple's documentation, the IIe also supports double low-resolution
|
||||
graphics, which are the 80-column analogue to ordinary low-resolution 40-column
|
||||
low-resolution graphics.
|
||||
*/
|
||||
void set_high_resolution(bool);
|
||||
bool get_high_resolution();
|
||||
|
||||
/*!
|
||||
Setter for annunciator 3.
|
||||
|
||||
* On: turn on annunciator 3.
|
||||
* Off: turn off annunciator 3.
|
||||
|
||||
This exists on both the II/II+ and the IIe, but has no effect on
|
||||
video on the older machines. It's intended to be used on the IIe
|
||||
to confirm double-high resolution mode but has side effects in
|
||||
selecting mixed mode output and discarding high-resolution
|
||||
delay bits.
|
||||
*/
|
||||
void set_annunciator_3(bool);
|
||||
bool get_annunciator_3();
|
||||
|
||||
// Setup for text mode.
|
||||
void set_character_rom(const std::vector<uint8_t> &);
|
||||
|
||||
protected:
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
// State affecting output video stream generation.
|
||||
uint8_t *pixel_pointer_ = nullptr;
|
||||
|
||||
// State affecting logical state.
|
||||
int row_ = 0, column_ = 0, flash_ = 0;
|
||||
uint8_t flash_mask() {
|
||||
return static_cast<uint8_t>((flash_ / flash_length) * 0xff);
|
||||
}
|
||||
|
||||
// Enumerates all Apple II and IIe display modes.
|
||||
enum class GraphicsMode {
|
||||
Text = 0,
|
||||
DoubleText,
|
||||
HighRes,
|
||||
DoubleHighRes,
|
||||
LowRes,
|
||||
DoubleLowRes,
|
||||
FatLowRes
|
||||
};
|
||||
bool is_text_mode(GraphicsMode m) { return m <= GraphicsMode::DoubleText; }
|
||||
bool is_double_mode(GraphicsMode m) { return !!(static_cast<int>(m)&1); }
|
||||
|
||||
// Various soft-switch values.
|
||||
bool alternative_character_set_ = false, set_alternative_character_set_ = false;
|
||||
bool columns_80_ = false, set_columns_80_ = false;
|
||||
bool store_80_ = false, set_store_80_ = false;
|
||||
bool page2_ = false, set_page2_ = false;
|
||||
bool text_ = true, set_text_ = true;
|
||||
bool mixed_ = false, set_mixed_ = false;
|
||||
bool high_resolution_ = false, set_high_resolution_ = false;
|
||||
bool annunciator_3_ = false, set_annunciator_3_ = false;
|
||||
|
||||
// Graphics carry is the final level output in a fetch window;
|
||||
// it carries on into the next if it's high resolution with
|
||||
// the delay bit set.
|
||||
mutable uint8_t graphics_carry_ = 0;
|
||||
bool was_double_ = false;
|
||||
uint8_t high_resolution_mask_ = 0xff;
|
||||
|
||||
// This holds a copy of the character ROM. The regular character
|
||||
// set is assumed to be in the first 64*8 bytes; the alternative
|
||||
// is in the 128*8 bytes after that.
|
||||
std::vector<uint8_t> character_rom_;
|
||||
|
||||
// Memory is fetched ahead of time into this array;
|
||||
// this permits the correct delay between fetching
|
||||
// without having to worry about a rolling buffer.
|
||||
std::array<uint8_t, 40> base_stream_;
|
||||
std::array<uint8_t, 40> auxiliary_stream_;
|
||||
|
||||
bool is_iie_ = false;
|
||||
static const int flash_length = 8406;
|
||||
|
||||
// Describes the current text mode mapping from in-memory character index
|
||||
// to output character.
|
||||
struct CharacterMapping {
|
||||
uint8_t address_mask;
|
||||
uint8_t xor_mask;
|
||||
};
|
||||
CharacterMapping character_zones[4];
|
||||
|
||||
/*!
|
||||
Outputs 40-column text to @c target, using @c length bytes from @c source.
|
||||
*/
|
||||
void output_text(uint8_t *target, const uint8_t *source, size_t length, size_t pixel_row) const;
|
||||
|
||||
/*!
|
||||
Outputs 80-column text to @c target, drawing @c length columns from @c source and @c auxiliary_source.
|
||||
*/
|
||||
void output_double_text(uint8_t *target, const uint8_t *source, const uint8_t *auxiliary_source, size_t length, size_t pixel_row) const;
|
||||
|
||||
/*!
|
||||
Outputs 40-column low-resolution graphics to @c target, drawing @c length columns from @c source.
|
||||
*/
|
||||
void output_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const;
|
||||
|
||||
/*!
|
||||
Outputs 80-column low-resolution graphics to @c target, drawing @c length columns from @c source and @c auxiliary_source.
|
||||
*/
|
||||
void output_double_low_resolution(uint8_t *target, const uint8_t *source, const uint8_t *auxiliary_source, size_t length, int column, int row) const;
|
||||
|
||||
/*!
|
||||
Outputs 40-column high-resolution graphics to @c target, drawing @c length columns from @c source.
|
||||
*/
|
||||
void output_high_resolution(uint8_t *target, const uint8_t *source, size_t length) const;
|
||||
|
||||
/*!
|
||||
Outputs 80-column double-high-resolution graphics to @c target, drawing @c length columns from @c source.
|
||||
*/
|
||||
void output_double_high_resolution(uint8_t *target, const uint8_t *source, const uint8_t *auxiliary_source, size_t length) const;
|
||||
|
||||
/*!
|
||||
Outputs 40-column "fat low resolution" graphics to @c target, drawing @c length columns from @c source.
|
||||
|
||||
Fat low-resolution mode is like regular low-resolution mode except that data is shifted out on the 7M
|
||||
clock rather than the 14M.
|
||||
*/
|
||||
void output_fat_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const;
|
||||
|
||||
// Maintain a DeferredQueue for delayed mode switches.
|
||||
DeferredQueue<Cycles> deferrer_;
|
||||
};
|
||||
|
||||
template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
public:
|
||||
/// Constructs an instance of the video feed; a CRT is also created.
|
||||
Video(BusHandler &bus_handler) :
|
||||
VideoBase(is_iie, [=] (Cycles cycles) { advance(cycles); }),
|
||||
bus_handler_(bus_handler) {}
|
||||
|
||||
/*!
|
||||
Runs video for @c cycles.
|
||||
*/
|
||||
void run_for(Cycles cycles) {
|
||||
deferrer_.run_for(cycles);
|
||||
}
|
||||
|
||||
/*!
|
||||
Obtains the last value the video read prior to time now+offset.
|
||||
*/
|
||||
uint8_t get_last_read_value(Cycles offset) {
|
||||
// Rules of generation:
|
||||
// (1) a complete sixty-five-cycle scan line consists of sixty-five consecutive bytes of
|
||||
// display buffer memory that starts twenty-five bytes prior to the actual data to be displayed.
|
||||
// (2) During VBL the data acts just as if it were starting a whole new frame from the beginning, but
|
||||
// it never finishes this pseudo-frame. After getting one third of the way through the frame (to
|
||||
// scan line $3F), it suddenly repeats the previous six scan lines ($3A through $3F) before aborting
|
||||
// to begin the next true frame.
|
||||
//
|
||||
// Source: Have an Apple Split by Bob Bishop; http://rich12345.tripod.com/aiivideo/softalk.html
|
||||
|
||||
// Determine column at offset.
|
||||
int mapped_column = column_ + offset.as_int();
|
||||
|
||||
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
||||
// (so what was column 0 is now column 25).
|
||||
mapped_column += 25;
|
||||
|
||||
// Apply carry into the row counter.
|
||||
int mapped_row = row_ + (mapped_column / 65);
|
||||
mapped_column %= 65;
|
||||
mapped_row %= 262;
|
||||
|
||||
// Apple out-of-bounds row logic.
|
||||
if(mapped_row >= 256) {
|
||||
mapped_row = 0x3a + (mapped_row&255);
|
||||
} else {
|
||||
mapped_row %= 192;
|
||||
}
|
||||
|
||||
// Calculate the address and return the value.
|
||||
uint16_t read_address = static_cast<uint16_t>(get_row_address(mapped_row) + mapped_column - 25);
|
||||
uint8_t value, aux_value;
|
||||
bus_handler_.perform_read(read_address, 1, &value, &aux_value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns @c true if the display will be within vertical blank at now + @c offset; @c false otherwise.
|
||||
*/
|
||||
bool get_is_vertical_blank(Cycles offset) {
|
||||
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
||||
// (so what was column 0 is now column 25).
|
||||
int mapped_column = column_ + offset.as_int();
|
||||
|
||||
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
||||
// (so what was column 0 is now column 25).
|
||||
mapped_column += 25;
|
||||
|
||||
// Apply carry into the row counter and test it for location.
|
||||
int mapped_row = row_ + (mapped_column / 65);
|
||||
return (mapped_row % 262) >= 192;
|
||||
}
|
||||
|
||||
private:
|
||||
/*!
|
||||
Advances time by @c cycles; expects to be fed by the CPU clock.
|
||||
Implicitly adds an extra half a colour clock at the end of
|
||||
line.
|
||||
*/
|
||||
void advance(Cycles cycles) {
|
||||
/*
|
||||
Addressing scheme used throughout is that column 0 is the first column with pixels in it;
|
||||
row 0 is the first row with pixels in it.
|
||||
|
||||
A frame is oriented around 65 cycles across, 262 lines down.
|
||||
*/
|
||||
static const int first_sync_line = 220; // A complete guess. Information needed.
|
||||
static const int first_sync_column = 49; // Also a guess.
|
||||
static const int sync_length = 4; // One of the two likely candidates.
|
||||
|
||||
int int_cycles = cycles.as_int();
|
||||
while(int_cycles) {
|
||||
const int cycles_this_line = std::min(65 - column_, int_cycles);
|
||||
const int ending_column = column_ + cycles_this_line;
|
||||
const bool is_vertical_sync_line = (row_ >= first_sync_line && row_ < first_sync_line + 3);
|
||||
|
||||
if(is_vertical_sync_line) {
|
||||
// In effect apply an XOR to HSYNC and VSYNC flags in order to include equalising
|
||||
// pulses (and hencce keep hsync approximately where it should be during vsync).
|
||||
const int blank_start = std::max(first_sync_column - sync_length, column_);
|
||||
const int blank_end = std::min(first_sync_column, ending_column);
|
||||
if(blank_end > blank_start) {
|
||||
if(blank_start > column_) {
|
||||
crt_.output_sync((blank_start - column_) * 14);
|
||||
}
|
||||
crt_.output_blank((blank_end - blank_start) * 14);
|
||||
if(blank_end < ending_column) {
|
||||
crt_.output_sync((ending_column - blank_end) * 14);
|
||||
}
|
||||
} else {
|
||||
crt_.output_sync(cycles_this_line * 14);
|
||||
}
|
||||
} else {
|
||||
const GraphicsMode line_mode = graphics_mode(row_);
|
||||
|
||||
// Determine whether there's any fetching to do. Fetching occurs during the first
|
||||
// 40 columns of rows prior to 192.
|
||||
if(row_ < 192 && column_ < 40) {
|
||||
const int character_row = row_ >> 3;
|
||||
const uint16_t row_address = static_cast<uint16_t>((character_row >> 3) * 40 + ((character_row&7) << 7));
|
||||
|
||||
// Grab the memory contents that'll be needed momentarily.
|
||||
const int fetch_end = std::min(40, ending_column);
|
||||
uint16_t fetch_address;
|
||||
switch(line_mode) {
|
||||
default:
|
||||
case GraphicsMode::Text:
|
||||
case GraphicsMode::DoubleText:
|
||||
case GraphicsMode::LowRes:
|
||||
case GraphicsMode::FatLowRes:
|
||||
case GraphicsMode::DoubleLowRes: {
|
||||
const uint16_t text_address = static_cast<uint16_t>(((video_page()+1) * 0x400) + row_address);
|
||||
fetch_address = static_cast<uint16_t>(text_address + column_);
|
||||
} break;
|
||||
|
||||
case GraphicsMode::HighRes:
|
||||
case GraphicsMode::DoubleHighRes:
|
||||
fetch_address = static_cast<uint16_t>(((video_page()+1) * 0x2000) + row_address + ((row_&7) << 10) + column_);
|
||||
break;
|
||||
}
|
||||
|
||||
bus_handler_.perform_read(
|
||||
fetch_address,
|
||||
static_cast<size_t>(fetch_end - column_),
|
||||
&base_stream_[static_cast<size_t>(column_)],
|
||||
&auxiliary_stream_[static_cast<size_t>(column_)]);
|
||||
}
|
||||
|
||||
if(row_ < 192) {
|
||||
// The pixel area is the first 40.5 columns; base contents
|
||||
// remain where they would naturally be but auxiliary
|
||||
// graphics appear to the left of that.
|
||||
if(!column_) {
|
||||
pixel_pointer_ = crt_.begin_data(568);
|
||||
graphics_carry_ = 0;
|
||||
was_double_ = true;
|
||||
}
|
||||
|
||||
if(column_ < 40) {
|
||||
const int pixel_start = std::max(0, column_);
|
||||
const int pixel_end = std::min(40, ending_column);
|
||||
const int pixel_row = row_ & 7;
|
||||
|
||||
const bool is_double = Video::is_double_mode(line_mode);
|
||||
if(!is_double && was_double_ && pixel_pointer_) {
|
||||
pixel_pointer_[pixel_start*14 + 0] =
|
||||
pixel_pointer_[pixel_start*14 + 1] =
|
||||
pixel_pointer_[pixel_start*14 + 2] =
|
||||
pixel_pointer_[pixel_start*14 + 3] =
|
||||
pixel_pointer_[pixel_start*14 + 4] =
|
||||
pixel_pointer_[pixel_start*14 + 5] =
|
||||
pixel_pointer_[pixel_start*14 + 6] = 0;
|
||||
}
|
||||
was_double_ = is_double;
|
||||
|
||||
if(pixel_pointer_) {
|
||||
switch(line_mode) {
|
||||
case GraphicsMode::Text:
|
||||
output_text(
|
||||
&pixel_pointer_[pixel_start * 14 + 7],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start),
|
||||
static_cast<size_t>(pixel_row));
|
||||
break;
|
||||
|
||||
case GraphicsMode::DoubleText:
|
||||
output_double_text(
|
||||
&pixel_pointer_[pixel_start * 14],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start),
|
||||
static_cast<size_t>(pixel_row));
|
||||
break;
|
||||
|
||||
case GraphicsMode::LowRes:
|
||||
output_low_resolution(
|
||||
&pixel_pointer_[pixel_start * 14 + 7],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start),
|
||||
pixel_start,
|
||||
pixel_row);
|
||||
break;
|
||||
|
||||
case GraphicsMode::FatLowRes:
|
||||
output_fat_low_resolution(
|
||||
&pixel_pointer_[pixel_start * 14 + 7],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start),
|
||||
pixel_start,
|
||||
pixel_row);
|
||||
break;
|
||||
|
||||
case GraphicsMode::DoubleLowRes:
|
||||
output_double_low_resolution(
|
||||
&pixel_pointer_[pixel_start * 14],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start),
|
||||
pixel_start,
|
||||
pixel_row);
|
||||
break;
|
||||
|
||||
case GraphicsMode::HighRes:
|
||||
output_high_resolution(
|
||||
&pixel_pointer_[pixel_start * 14 + 7],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start));
|
||||
break;
|
||||
|
||||
case GraphicsMode::DoubleHighRes:
|
||||
output_double_high_resolution(
|
||||
&pixel_pointer_[pixel_start * 14],
|
||||
&base_stream_[static_cast<size_t>(pixel_start)],
|
||||
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
|
||||
static_cast<size_t>(pixel_end - pixel_start));
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
if(pixel_end == 40) {
|
||||
if(pixel_pointer_) {
|
||||
if(was_double_) {
|
||||
pixel_pointer_[560] = pixel_pointer_[561] = pixel_pointer_[562] = pixel_pointer_[563] =
|
||||
pixel_pointer_[564] = pixel_pointer_[565] = pixel_pointer_[566] = pixel_pointer_[567] = 0;
|
||||
} else {
|
||||
if(line_mode == GraphicsMode::HighRes && base_stream_[39]&0x80)
|
||||
pixel_pointer_[567] = graphics_carry_;
|
||||
else
|
||||
pixel_pointer_[567] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
crt_.output_data(568, 568);
|
||||
pixel_pointer_ = nullptr;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(column_ < 40 && ending_column >= 40) {
|
||||
crt_.output_blank(568);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The left border, sync, right border pattern doesn't depend on whether
|
||||
there were pixels this row and is output as soon as it is known.
|
||||
*/
|
||||
|
||||
if(column_ < first_sync_column && ending_column >= first_sync_column) {
|
||||
crt_.output_blank(first_sync_column*14 - 568);
|
||||
}
|
||||
|
||||
if(column_ < (first_sync_column + sync_length) && ending_column >= (first_sync_column + sync_length)) {
|
||||
crt_.output_sync(sync_length*14);
|
||||
}
|
||||
|
||||
int second_blank_start;
|
||||
if(!is_text_mode(graphics_mode(row_+1))) {
|
||||
const int colour_burst_start = std::max(first_sync_column + sync_length + 1, column_);
|
||||
const int colour_burst_end = std::min(first_sync_column + sync_length + 4, ending_column);
|
||||
if(colour_burst_end > colour_burst_start) {
|
||||
crt_.output_colour_burst((colour_burst_end - colour_burst_start) * 14, 0);
|
||||
}
|
||||
|
||||
second_blank_start = std::max(first_sync_column + sync_length + 3, column_);
|
||||
} else {
|
||||
second_blank_start = std::max(first_sync_column + sync_length, column_);
|
||||
}
|
||||
|
||||
if(ending_column > second_blank_start) {
|
||||
crt_.output_blank((ending_column - second_blank_start) * 14);
|
||||
}
|
||||
}
|
||||
|
||||
int_cycles -= cycles_this_line;
|
||||
column_ = (column_ + cycles_this_line) % 65;
|
||||
if(!column_) {
|
||||
row_ = (row_ + 1) % 262;
|
||||
flash_ = (flash_ + 1) % (2 * flash_length);
|
||||
if(!alternative_character_set_) {
|
||||
character_zones[1].xor_mask = flash_mask();
|
||||
}
|
||||
|
||||
// Add an extra half a colour cycle of blank; this isn't counted in the run_for
|
||||
// count explicitly but is promised. If this is a vertical sync line, output sync
|
||||
// instead of blank, taking that to be the default level.
|
||||
if(is_vertical_sync_line) {
|
||||
crt_.output_sync(2);
|
||||
} else {
|
||||
crt_.output_blank(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GraphicsMode graphics_mode(int row) {
|
||||
if(
|
||||
text_ ||
|
||||
(mixed_ && row >= 160 && row < 192)
|
||||
) return columns_80_ ? GraphicsMode::DoubleText : GraphicsMode::Text;
|
||||
if(high_resolution_) {
|
||||
return (annunciator_3_ && columns_80_) ? GraphicsMode::DoubleHighRes : GraphicsMode::HighRes;
|
||||
} else {
|
||||
if(columns_80_) return GraphicsMode::DoubleLowRes;
|
||||
if(annunciator_3_) return GraphicsMode::FatLowRes;
|
||||
return GraphicsMode::LowRes;
|
||||
}
|
||||
}
|
||||
|
||||
int video_page() {
|
||||
return (store_80_ || !page2_) ? 0 : 1;
|
||||
}
|
||||
|
||||
uint16_t get_row_address(int row) {
|
||||
const int character_row = row >> 3;
|
||||
const int pixel_row = row & 7;
|
||||
const uint16_t row_address = static_cast<uint16_t>((character_row >> 3) * 40 + ((character_row&7) << 7));
|
||||
|
||||
const GraphicsMode pixel_mode = graphics_mode(row);
|
||||
return ((pixel_mode == GraphicsMode::HighRes) || (pixel_mode == GraphicsMode::DoubleHighRes)) ?
|
||||
static_cast<uint16_t>(((video_page()+1) * 0x2000) + row_address + ((pixel_row&7) << 10)) :
|
||||
static_cast<uint16_t>(((video_page()+1) * 0x400) + row_address);
|
||||
}
|
||||
|
||||
BusHandler &bus_handler_;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Video_hpp */
|
||||
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 */
|
||||
@@ -1,722 +0,0 @@
|
||||
//
|
||||
// AppleII.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/04/2018.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#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 "../../Processors/6502/6502.hpp"
|
||||
#include "../../Components/AudioToggle/AudioToggle.hpp"
|
||||
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
|
||||
#include "Card.hpp"
|
||||
#include "DiskIICard.hpp"
|
||||
#include "Video.hpp"
|
||||
|
||||
#include "../../Analyser/Static/AppleII/Target.hpp"
|
||||
#include "../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../Configurable/Configurable.hpp"
|
||||
#include "../../Storage/Disk/Track/TrackSerialiser.hpp"
|
||||
#include "../../Storage/Disk/Encodings/AppleGCR/SegmentParser.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> AppleII::get_options() {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> options;
|
||||
options.emplace_back(new Configurable::BooleanOption("Accelerate DOS 3.3", "quickload"));
|
||||
return options;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
class ConcreteMachine:
|
||||
public CRTMachine::Machine,
|
||||
public MediaTarget::Machine,
|
||||
public KeyboardMachine::Machine,
|
||||
public Configurable::Device,
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public Inputs::Keyboard,
|
||||
public AppleII::Machine,
|
||||
public Activity::Source,
|
||||
public JoystickMachine::Machine,
|
||||
public AppleII::Card::Delegate {
|
||||
private:
|
||||
struct VideoBusHandler : public AppleII::Video::BusHandler {
|
||||
public:
|
||||
VideoBusHandler(uint8_t *ram) : ram_(ram) {}
|
||||
|
||||
uint8_t perform_read(uint16_t address) {
|
||||
return ram_[address];
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t *ram_;
|
||||
};
|
||||
|
||||
CPU::MOS6502::Processor<ConcreteMachine, false> m6502_;
|
||||
VideoBusHandler video_bus_handler_;
|
||||
std::unique_ptr<AppleII::Video::Video<VideoBusHandler>> video_;
|
||||
int cycles_into_current_line_ = 0;
|
||||
Cycles cycles_since_video_update_;
|
||||
|
||||
void update_video() {
|
||||
video_->run_for(cycles_since_video_update_.flush());
|
||||
}
|
||||
static const int audio_divider = 8;
|
||||
void update_audio() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider)));
|
||||
}
|
||||
void update_just_in_time_cards() {
|
||||
for(const auto &card : just_in_time_cards_) {
|
||||
card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_);
|
||||
}
|
||||
cycles_since_card_update_ = 0;
|
||||
stretched_cycles_since_card_update_ = 0;
|
||||
}
|
||||
|
||||
uint8_t ram_[65536], aux_ram_[65536];
|
||||
std::vector<uint8_t> rom_;
|
||||
std::vector<uint8_t> character_rom_;
|
||||
uint8_t keyboard_input_ = 0x00;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
Outputs::Speaker::LowpassSpeaker<Audio::Toggle> speaker_;
|
||||
Cycles cycles_since_audio_update_;
|
||||
|
||||
// MARK: - Cards
|
||||
std::array<std::unique_ptr<AppleII::Card>, 7> cards_;
|
||||
Cycles cycles_since_card_update_;
|
||||
std::vector<AppleII::Card *> every_cycle_cards_;
|
||||
std::vector<AppleII::Card *> just_in_time_cards_;
|
||||
|
||||
int stretched_cycles_since_card_update_ = 0;
|
||||
|
||||
void install_card(std::size_t slot, AppleII::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) {
|
||||
return !card->get_select_constraints();
|
||||
}
|
||||
|
||||
void pick_card_messaging_group(AppleII::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_;
|
||||
|
||||
if(std::find(intended.begin(), intended.end(), card) != intended.end()) return;
|
||||
auto old_membership = std::find(undesired.begin(), undesired.end(), card);
|
||||
if(old_membership != undesired.end()) undesired.erase(old_membership);
|
||||
intended.push_back(card);
|
||||
}
|
||||
|
||||
void card_did_change_select_constraints(AppleII::Card *card) override {
|
||||
pick_card_messaging_group(card);
|
||||
}
|
||||
|
||||
AppleII::DiskIICard *diskii_card() {
|
||||
return dynamic_cast<AppleII::DiskIICard *>(cards_[5].get());
|
||||
}
|
||||
|
||||
// MARK: - Memory Map
|
||||
struct MemoryBlock {
|
||||
uint8_t *read_pointer = nullptr;
|
||||
uint8_t *write_pointer = nullptr;
|
||||
} memory_blocks_[4]; // The IO page isn't included.
|
||||
|
||||
// MARK: - The language card.
|
||||
struct {
|
||||
bool bank1 = false;
|
||||
bool read = false;
|
||||
bool pre_write = false;
|
||||
bool write = false;
|
||||
} language_card_;
|
||||
bool has_language_card_ = true;
|
||||
void set_language_card_paging() {
|
||||
if(has_language_card_ && !language_card_.write) {
|
||||
memory_blocks_[2].write_pointer = &ram_[48*1024 + (language_card_.bank1 ? 0x1000 : 0x0000)];
|
||||
memory_blocks_[3].write_pointer = &ram_[56*1024];
|
||||
} else {
|
||||
memory_blocks_[2].write_pointer = memory_blocks_[3].write_pointer = nullptr;
|
||||
}
|
||||
|
||||
if(has_language_card_ && language_card_.read) {
|
||||
memory_blocks_[2].read_pointer = &ram_[48*1024 + (language_card_.bank1 ? 0x1000 : 0x0000)];
|
||||
memory_blocks_[3].read_pointer = &ram_[56*1024];
|
||||
} else {
|
||||
memory_blocks_[2].read_pointer = rom_.data();
|
||||
memory_blocks_[3].read_pointer = rom_.data() + 0x1000;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK - typing
|
||||
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
|
||||
|
||||
// MARK - quick loading
|
||||
bool should_load_quickly_ = false;
|
||||
|
||||
// MARK - joysticks
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Horizontal),
|
||||
Input(Input::Vertical),
|
||||
|
||||
// The Apple II offers three buttons between two joysticks;
|
||||
// this emulator puts three buttons on each joystick and
|
||||
// combines them.
|
||||
Input(Input::Fire, 0),
|
||||
Input(Input::Fire, 1),
|
||||
Input(Input::Fire, 2),
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &input, float value) override {
|
||||
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical))
|
||||
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
|
||||
}
|
||||
|
||||
void did_set_input(const Input &input, bool value) override {
|
||||
if(input.type == Input::Type::Fire && input.info.control.index < 3) {
|
||||
buttons[input.info.control.index] = value;
|
||||
}
|
||||
}
|
||||
|
||||
bool buttons[3] = {false, false, false};
|
||||
float axes[2] = {0.5f, 0.5f};
|
||||
};
|
||||
|
||||
// On an Apple II, the programmer strobes 0xc070 and that causes each analogue input
|
||||
// to begin a charge and discharge cycle **if they are not already charging**.
|
||||
// The greater the analogue input, the faster they will charge and therefore the sooner
|
||||
// they will discharge.
|
||||
//
|
||||
// This emulator models that with analogue_charge_ being essentially the amount of time,
|
||||
// in charge threshold units, since 0xc070 was last strobed. But if any of the analogue
|
||||
// inputs were already partially charged then they gain a bias in analogue_biases_.
|
||||
//
|
||||
// It's a little indirect, but it means only having to increment the one value in the
|
||||
// main loop.
|
||||
float analogue_charge_ = 0.0f;
|
||||
float analogue_biases_[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
bool analogue_channel_is_discharged(size_t channel) {
|
||||
return static_cast<Joystick *>(joysticks_[channel >> 1].get())->axes[channel & 1] < analogue_charge_ + analogue_biases_[channel];
|
||||
}
|
||||
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::AppleII::Target &target, const ROMMachine::ROMFetcher &rom_fetcher):
|
||||
m6502_(*this),
|
||||
video_bus_handler_(ram_),
|
||||
audio_toggle_(audio_queue_),
|
||||
speaker_(audio_toggle_) {
|
||||
// The system's master clock rate.
|
||||
const float master_clock = 14318180.0;
|
||||
|
||||
// This is where things get slightly convoluted: establish the machine as having a clock rate
|
||||
// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
|
||||
// the master clock rate divided by 14 because every 65th cycle is extended by one seventh.
|
||||
set_clock_rate((master_clock / 14.0) * 65.0 / (65.0 + 1.0 / 7.0));
|
||||
|
||||
// The speaker, however, should think it is clocked at half the master clock, per a general
|
||||
// decision to sample it at seven times the CPU clock (plus stretches).
|
||||
speaker_.set_input_rate(static_cast<float>(master_clock / (2.0 * static_cast<float>(audio_divider))));
|
||||
|
||||
// Apply a 6Khz low-pass filter. This was picked by ear and by an attempt to understand the
|
||||
// Apple II schematic but, well, I don't claim much insight on the latter. This is definitely
|
||||
// something to review in the future.
|
||||
speaker_.set_high_frequency_cutoff(6000);
|
||||
|
||||
// Also, start with randomised memory contents.
|
||||
Memory::Fuzz(ram_, sizeof(ram_));
|
||||
|
||||
// Add a couple of joysticks.
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
|
||||
// Pick the required ROMs.
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
std::vector<std::string> rom_names = {"apple2-character.rom"};
|
||||
switch(target.model) {
|
||||
default:
|
||||
rom_names.push_back("apple2o.rom");
|
||||
break;
|
||||
case Target::Model::IIplus:
|
||||
rom_names.push_back("apple2.rom");
|
||||
break;
|
||||
}
|
||||
const auto roms = rom_fetcher("AppleII", rom_names);
|
||||
|
||||
if(!roms[0] || !roms[1]) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
character_rom_ = std::move(*roms[0]);
|
||||
rom_ = std::move(*roms[1]);
|
||||
if(rom_.size() > 12*1024) {
|
||||
rom_.erase(rom_.begin(), rom_.begin() + static_cast<off_t>(rom_.size()) - 12*1024);
|
||||
}
|
||||
|
||||
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.
|
||||
memory_blocks_[0].read_pointer = memory_blocks_[0].write_pointer = ram_;
|
||||
memory_blocks_[1].read_pointer = memory_blocks_[1].write_pointer = &ram_[0x200];
|
||||
set_language_card_paging();
|
||||
|
||||
insert_media(target.media);
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
void setup_output(float aspect_ratio) override {
|
||||
video_.reset(new AppleII::Video::Video<VideoBusHandler>(video_bus_handler_));
|
||||
video_->set_character_rom(character_rom_);
|
||||
}
|
||||
|
||||
void close_output() override {
|
||||
video_.reset();
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *get_crt() override {
|
||||
return video_->get_crt();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
forceinline Cycles perform_bus_operation(const CPU::MOS6502::BusOperation operation, const uint16_t address, uint8_t *const value) {
|
||||
++ cycles_since_video_update_;
|
||||
++ cycles_since_card_update_;
|
||||
cycles_since_audio_update_ += Cycles(7);
|
||||
|
||||
// The Apple II has a slightly weird timing pattern: every 65th CPU cycle is stretched
|
||||
// by an extra 1/7th. That's because one cycle lasts 3.5 NTSC colour clocks, so after
|
||||
// 65 cycles a full line of 227.5 colour clocks have passed. But the high-rate binary
|
||||
// signal approximation that produces colour needs to be in phase, so a stretch of exactly
|
||||
// 0.5 further colour cycles is added. The video class handles that implicitly, but it
|
||||
// needs to be accumulated here for the audio.
|
||||
cycles_into_current_line_ = (cycles_into_current_line_ + 1) % 65;
|
||||
const bool is_stretched_cycle = !cycles_into_current_line_;
|
||||
if(is_stretched_cycle) {
|
||||
++ cycles_since_audio_update_;
|
||||
++ stretched_cycles_since_card_update_;
|
||||
}
|
||||
|
||||
/*
|
||||
There are five distinct zones of memory on an Apple II:
|
||||
|
||||
0000 to 0200 : the zero and stack pages, which can be paged independently on a IIe
|
||||
0200 to c000 : the main block of RAM, which can be paged on a IIe
|
||||
c000 to d000 : the IO area, including card ROMs
|
||||
d000 to e000 : the low ROM area, which can contain indepdently-paged RAM with a language card
|
||||
e000 onward : the rest of ROM, also potentially replaced with RAM by a language card
|
||||
*/
|
||||
uint16_t accessed_address = address;
|
||||
MemoryBlock *block = nullptr;
|
||||
if(address < 0x200) block = &memory_blocks_[0];
|
||||
else if(address < 0xc000) {
|
||||
if(address < 0x6000 && !isReadOperation(operation)) update_video();
|
||||
block = &memory_blocks_[1];
|
||||
accessed_address -= 0x200;
|
||||
}
|
||||
else if(address < 0xd000) block = nullptr;
|
||||
else if(address < 0xe000) {block = &memory_blocks_[2]; accessed_address -= 0xd000; }
|
||||
else { block = &memory_blocks_[3]; accessed_address -= 0xe000; }
|
||||
|
||||
bool has_updated_cards = false;
|
||||
if(block) {
|
||||
if(isReadOperation(operation)) *value = block->read_pointer[accessed_address];
|
||||
else if(block->write_pointer) block->write_pointer[accessed_address] = *value;
|
||||
|
||||
if(should_load_quickly_) {
|
||||
// Check for a prima facie entry into RWTS.
|
||||
if(operation == CPU::MOS6502::BusOperation::ReadOpcode && address == 0xb7b5) {
|
||||
// Grab the IO control block address for inspection.
|
||||
uint16_t io_control_block_address =
|
||||
static_cast<uint16_t>(
|
||||
(m6502_.get_value_of_register(CPU::MOS6502::Register::A) << 8) |
|
||||
m6502_.get_value_of_register(CPU::MOS6502::Register::Y)
|
||||
);
|
||||
|
||||
// Verify that this is table type one, for execution on card six,
|
||||
// against drive 1 or 2, and that the command is either a seek or a sector read.
|
||||
if(
|
||||
ram_[io_control_block_address+0x00] == 0x01 &&
|
||||
ram_[io_control_block_address+0x01] == 0x60 &&
|
||||
ram_[io_control_block_address+0x02] > 0 && ram_[io_control_block_address+0x02] < 3 &&
|
||||
ram_[io_control_block_address+0x0c] < 2
|
||||
) {
|
||||
const uint8_t iob_track = ram_[io_control_block_address+4];
|
||||
const uint8_t iob_sector = ram_[io_control_block_address+5];
|
||||
const uint8_t iob_drive = ram_[io_control_block_address+2] - 1;
|
||||
|
||||
// Get the track identified and store the new head position.
|
||||
auto track = diskii_card()->get_drive(iob_drive).step_to(Storage::Disk::HeadPosition(iob_track));
|
||||
|
||||
// DOS 3.3 keeps the current track (unspecified drive) in 0x478; the current track for drive 1 and drive 2
|
||||
// is also kept in that Disk II card's screen hole.
|
||||
ram_[0x478] = iob_track;
|
||||
if(ram_[io_control_block_address+0x02] == 1) {
|
||||
ram_[0x47e] = iob_track;
|
||||
} else {
|
||||
ram_[0x4fe] = iob_track;
|
||||
}
|
||||
|
||||
// Check whether this is a read, not merely a seek.
|
||||
if(ram_[io_control_block_address+0x0c] == 1) {
|
||||
// Apple the DOS 3.3 formula to map the requested logical sector to a physical sector.
|
||||
const int physical_sector = (iob_sector == 15) ? 15 : ((iob_sector * 13) % 15);
|
||||
|
||||
// Parse the entire track. TODO: cache these.
|
||||
auto sector_map = Storage::Encodings::AppleGCR::sectors_from_segment(
|
||||
Storage::Disk::track_serialisation(*track, Storage::Time(1, 50000)));
|
||||
|
||||
bool found_sector = false;
|
||||
for(const auto &pair: sector_map) {
|
||||
if(pair.second.address.sector == physical_sector) {
|
||||
found_sector = true;
|
||||
|
||||
// Copy the sector contents to their destination.
|
||||
uint16_t target = static_cast<uint16_t>(
|
||||
ram_[io_control_block_address+8] |
|
||||
(ram_[io_control_block_address+9] << 8)
|
||||
);
|
||||
|
||||
for(size_t c = 0; c < 256; ++c) {
|
||||
ram_[target] = pair.second.data[c];
|
||||
++target;
|
||||
}
|
||||
|
||||
// Set no error encountered.
|
||||
ram_[io_control_block_address + 0xd] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(found_sector) {
|
||||
// Set no error in the flags register too, and RTS.
|
||||
m6502_.set_value_of_register(CPU::MOS6502::Register::Flags, m6502_.get_value_of_register(CPU::MOS6502::Register::Flags) & ~1);
|
||||
*value = 0x60;
|
||||
}
|
||||
} else {
|
||||
// No error encountered; RTS.
|
||||
m6502_.set_value_of_register(CPU::MOS6502::Register::Flags, m6502_.get_value_of_register(CPU::MOS6502::Register::Flags) & ~1);
|
||||
*value = 0x60;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Assume a vapour read unless it turns out otherwise; this is a little
|
||||
// wasteful but works for now.
|
||||
//
|
||||
// Longer version: like many other machines, when the Apple II reads from
|
||||
// an address at which no hardware loads the data bus, through a process of
|
||||
// practical analogue effects it'll end up receiving whatever was last on
|
||||
// the bus. Which will always be whatever the video circuit fetched because
|
||||
// that fetches in between every instruction.
|
||||
//
|
||||
// So this code assumes that'll happen unless it later determines that it
|
||||
// doesn't. The call into the video isn't free because it's a just-in-time
|
||||
// actor, but this will actually be the result most of the time so it's not
|
||||
// too terrible.
|
||||
if(isReadOperation(operation) && address != 0xc000) {
|
||||
*value = video_->get_last_read_value(cycles_since_video_update_);
|
||||
}
|
||||
|
||||
switch(address) {
|
||||
default:
|
||||
if(isReadOperation(operation)) {
|
||||
// Read-only switches.
|
||||
switch(address) {
|
||||
default: break;
|
||||
|
||||
case 0xc000:
|
||||
if(string_serialiser_) {
|
||||
*value = string_serialiser_->head() | 0x80;
|
||||
} else {
|
||||
*value = keyboard_input_;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc061: // Switch input 0.
|
||||
*value &= 0x7f;
|
||||
if(static_cast<Joystick *>(joysticks_[0].get())->buttons[0] || static_cast<Joystick *>(joysticks_[1].get())->buttons[2])
|
||||
*value |= 0x80;
|
||||
break;
|
||||
case 0xc062: // Switch input 1.
|
||||
*value &= 0x7f;
|
||||
if(static_cast<Joystick *>(joysticks_[0].get())->buttons[1] || static_cast<Joystick *>(joysticks_[1].get())->buttons[1])
|
||||
*value |= 0x80;
|
||||
break;
|
||||
case 0xc063: // Switch input 2.
|
||||
*value &= 0x7f;
|
||||
if(static_cast<Joystick *>(joysticks_[0].get())->buttons[2] || static_cast<Joystick *>(joysticks_[1].get())->buttons[0])
|
||||
*value |= 0x80;
|
||||
break;
|
||||
|
||||
case 0xc064: // Analogue input 0.
|
||||
case 0xc065: // Analogue input 1.
|
||||
case 0xc066: // Analogue input 2.
|
||||
case 0xc067: { // Analogue input 3.
|
||||
const size_t input = address - 0xc064;
|
||||
*value &= 0x7f;
|
||||
if(analogue_channel_is_discharged(input)) {
|
||||
*value |= 0x80;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
} else {
|
||||
// Write-only switches.
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc070: { // Permit analogue inputs that are currently discharged to begin a charge cycle.
|
||||
// Ensure those that were still charging retain that state.
|
||||
for(size_t c = 0; c < 4; ++c) {
|
||||
if(analogue_channel_is_discharged(c)) {
|
||||
analogue_biases_[c] = 0.0f;
|
||||
} else {
|
||||
analogue_biases_[c] += analogue_charge_;
|
||||
}
|
||||
}
|
||||
analogue_charge_ = 0.0f;
|
||||
} break;
|
||||
|
||||
/* Read-write switches. */
|
||||
case 0xc050: update_video(); video_->set_graphics_mode(); break;
|
||||
case 0xc051: update_video(); video_->set_text_mode(); break;
|
||||
case 0xc052: update_video(); video_->set_mixed_mode(false); break;
|
||||
case 0xc053: update_video(); video_->set_mixed_mode(true); break;
|
||||
case 0xc054: update_video(); video_->set_video_page(0); break;
|
||||
case 0xc055: update_video(); video_->set_video_page(1); break;
|
||||
case 0xc056: update_video(); video_->set_low_resolution(); break;
|
||||
case 0xc057: update_video(); video_->set_high_resolution(); break;
|
||||
|
||||
case 0xc010:
|
||||
keyboard_input_ &= 0x7f;
|
||||
if(string_serialiser_) {
|
||||
if(!string_serialiser_->advance())
|
||||
string_serialiser_.reset();
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc030:
|
||||
update_audio();
|
||||
audio_toggle_.set_output(!audio_toggle_.get_output());
|
||||
break;
|
||||
|
||||
case 0xc080: case 0xc084: case 0xc088: case 0xc08c:
|
||||
case 0xc081: case 0xc085: case 0xc089: case 0xc08d:
|
||||
case 0xc082: case 0xc086: case 0xc08a: case 0xc08e:
|
||||
case 0xc083: case 0xc087: case 0xc08b: case 0xc08f:
|
||||
// Quotes below taken from Understanding the Apple II, p. 5-28 and 5-29.
|
||||
|
||||
// "A3 controls the 4K bank selection"
|
||||
language_card_.bank1 = (address&8);
|
||||
|
||||
// "Access to $C080, $C083, $C084, $0087, $C088, $C08B, $C08C, or $C08F sets the READ ENABLE flip-flop"
|
||||
// (other accesses reset it)
|
||||
language_card_.read = !(((address&2) >> 1) ^ (address&1));
|
||||
|
||||
// "The WRITE ENABLE' flip-flop is reset by an odd read access to the $C08X range when the PRE-WRITE flip-flop is set."
|
||||
if(language_card_.pre_write && isReadOperation(operation) && (address&1)) language_card_.write = false;
|
||||
|
||||
// "[The WRITE ENABLE' flip-flop] is set by an even access in the $C08X range."
|
||||
if(!(address&1)) language_card_.write = true;
|
||||
|
||||
// ("Any other type of access causes the WRITE ENABLE' flip-flop to hold its current state.")
|
||||
|
||||
// "The PRE-WRITE flip-flop is set by an odd read access in the $C08X range. It is reset by an even access or a write access."
|
||||
language_card_.pre_write = isReadOperation(operation) ? (address&1) : false;
|
||||
|
||||
set_language_card_paging();
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
Communication with cards follows.
|
||||
*/
|
||||
|
||||
if(address >= 0xc090 && address < 0xc800) {
|
||||
// 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;
|
||||
|
||||
if(address >= 0xc100) {
|
||||
/*
|
||||
Decode the area conventionally used by cards for ROMs:
|
||||
0xCn00 to 0xCnff: card n.
|
||||
*/
|
||||
card_number = (address - 0xc100) >> 8;
|
||||
select = AppleII::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;
|
||||
}
|
||||
|
||||
// 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_[card_number].get();
|
||||
if(target && !is_every_cycle_card(target)) {
|
||||
update_just_in_time_cards();
|
||||
target->perform_bus_operation(select, is_read, address, value);
|
||||
}
|
||||
|
||||
// Update all the every-cycle cards regardless, but send them a ::None select if they're
|
||||
// not the one actually selected.
|
||||
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,
|
||||
is_read, address, value);
|
||||
}
|
||||
has_updated_cards = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!has_updated_cards && !every_cycle_cards_.empty()) {
|
||||
// Update all every-cycle cards and give them the cycle.
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Update analogue charge level.
|
||||
analogue_charge_ = std::min(analogue_charge_ + 1.0f / 2820.0f, 1.1f);
|
||||
|
||||
return Cycles(1);
|
||||
}
|
||||
|
||||
void flush() {
|
||||
update_video();
|
||||
update_audio();
|
||||
update_just_in_time_cards();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void set_key_pressed(Key key, char value, bool is_pressed) override {
|
||||
if(key == Key::F12) {
|
||||
m6502_.set_reset_line(is_pressed);
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_pressed) {
|
||||
// If no ASCII value is supplied, look for a few special cases.
|
||||
if(!value) {
|
||||
switch(key) {
|
||||
case Key::Left: value = 8; break;
|
||||
case Key::Right: value = 21; break;
|
||||
case Key::Down: value = 10; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
keyboard_input_ = static_cast<uint8_t>(toupper(value) | 0x80);
|
||||
}
|
||||
}
|
||||
|
||||
Inputs::Keyboard &get_keyboard() override {
|
||||
return *this;
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) override {
|
||||
string_serialiser_.reset(new Utility::StringSerialiser(string, true));
|
||||
}
|
||||
|
||||
// MARK: MediaTarget
|
||||
bool insert_media(const Analyser::Static::Media &media) override {
|
||||
if(!media.disks.empty()) {
|
||||
auto diskii = diskii_card();
|
||||
if(diskii) diskii->set_disk(media.disks[0], 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: Activity::Source
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
for(const auto &card: cards_) {
|
||||
if(card) card->set_activity_observer(observer);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Options
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
return AppleII::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
bool quickload;
|
||||
if(Configurable::get_quick_load_tape(selections_by_option, quickload)) {
|
||||
should_load_quickly_ = quickload;
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, false);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, true);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
// MARK: JoystickMachine
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
return joysticks_;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
using namespace AppleII;
|
||||
|
||||
Machine *Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
const Target *const appleii_target = dynamic_cast<const Target *>(target);
|
||||
return new ConcreteMachine(*appleii_target, rom_fetcher);
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
//
|
||||
// Video.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/04/2018.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Video.hpp"
|
||||
|
||||
using namespace AppleII::Video;
|
||||
|
||||
VideoBase::VideoBase() :
|
||||
crt_(new Outputs::CRT::CRT(910, 1, Outputs::CRT::DisplayType::NTSC60, 1)) {
|
||||
|
||||
// Set a composite sampling function that assumes one byte per pixel input, and
|
||||
// accepts any non-zero value as being fully on, zero being fully off.
|
||||
crt_->set_composite_sampling_function(
|
||||
"float composite_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate, float phase, float amplitude)"
|
||||
"{"
|
||||
"return texture(sampler, coordinate).r;"
|
||||
"}");
|
||||
|
||||
// Show only the centre 75% of the TV frame.
|
||||
crt_->set_video_signal(Outputs::CRT::VideoSignal::Composite);
|
||||
crt_->set_visible_area(Outputs::CRT::Rect(0.115f, 0.122f, 0.77f, 0.77f));
|
||||
crt_->set_immediate_default_phase(0.0f);
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *VideoBase::get_crt() {
|
||||
return crt_.get();
|
||||
}
|
||||
|
||||
void VideoBase::set_graphics_mode() {
|
||||
use_graphics_mode_ = true;
|
||||
}
|
||||
|
||||
void VideoBase::set_text_mode() {
|
||||
use_graphics_mode_ = false;
|
||||
}
|
||||
|
||||
void VideoBase::set_mixed_mode(bool mixed_mode) {
|
||||
mixed_mode_ = mixed_mode;
|
||||
}
|
||||
|
||||
void VideoBase::set_video_page(int page) {
|
||||
video_page_ = page;
|
||||
}
|
||||
|
||||
void VideoBase::set_low_resolution() {
|
||||
graphics_mode_ = GraphicsMode::LowRes;
|
||||
}
|
||||
|
||||
void VideoBase::set_high_resolution() {
|
||||
graphics_mode_ = GraphicsMode::HighRes;
|
||||
}
|
||||
|
||||
void VideoBase::set_character_rom(const std::vector<uint8_t> &character_rom) {
|
||||
character_rom_ = character_rom;
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
//
|
||||
// Video.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/04/2018.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Video_hpp
|
||||
#define Video_hpp
|
||||
|
||||
#include "../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace AppleII {
|
||||
namespace Video {
|
||||
|
||||
class BusHandler {
|
||||
public:
|
||||
uint8_t perform_read(uint16_t address) {
|
||||
return 0xff;
|
||||
}
|
||||
};
|
||||
|
||||
class VideoBase {
|
||||
public:
|
||||
VideoBase();
|
||||
|
||||
/// @returns The CRT this video feed is feeding.
|
||||
Outputs::CRT::CRT *get_crt();
|
||||
|
||||
// Inputs for the various soft switches.
|
||||
void set_graphics_mode();
|
||||
void set_text_mode();
|
||||
void set_mixed_mode(bool);
|
||||
void set_video_page(int);
|
||||
void set_low_resolution();
|
||||
void set_high_resolution();
|
||||
|
||||
// Setup for text mode.
|
||||
void set_character_rom(const std::vector<uint8_t> &);
|
||||
|
||||
protected:
|
||||
std::unique_ptr<Outputs::CRT::CRT> crt_;
|
||||
|
||||
uint8_t *pixel_pointer_ = nullptr;
|
||||
int pixel_pointer_column_ = 0;
|
||||
bool pixels_are_high_density_ = false;
|
||||
|
||||
int video_page_ = 0;
|
||||
int row_ = 0, column_ = 0, flash_ = 0;
|
||||
std::vector<uint8_t> character_rom_;
|
||||
|
||||
enum class GraphicsMode {
|
||||
LowRes,
|
||||
HighRes,
|
||||
Text
|
||||
} graphics_mode_ = GraphicsMode::LowRes;
|
||||
bool use_graphics_mode_ = false;
|
||||
bool mixed_mode_ = false;
|
||||
uint8_t graphics_carry_ = 0;
|
||||
};
|
||||
|
||||
template <class BusHandler> class Video: public VideoBase {
|
||||
public:
|
||||
/// Constructs an instance of the video feed; a CRT is also created.
|
||||
Video(BusHandler &bus_handler) :
|
||||
VideoBase(),
|
||||
bus_handler_(bus_handler) {}
|
||||
|
||||
/*!
|
||||
Advances time by @c cycles; expects to be fed by the CPU clock.
|
||||
Implicitly adds an extra half a colour clock at the end of every
|
||||
line.
|
||||
*/
|
||||
void run_for(const Cycles cycles) {
|
||||
/*
|
||||
Addressing scheme used throughout is that column 0 is the first column with pixels in it;
|
||||
row 0 is the first row with pixels in it.
|
||||
|
||||
A frame is oriented around 65 cycles across, 262 lines down.
|
||||
*/
|
||||
static const int first_sync_line = 220; // A complete guess. Information needed.
|
||||
static const int first_sync_column = 49; // Also a guess.
|
||||
static const int sync_length = 4; // One of the two likely candidates.
|
||||
|
||||
int int_cycles = cycles.as_int();
|
||||
while(int_cycles) {
|
||||
const int cycles_this_line = std::min(65 - column_, int_cycles);
|
||||
const int ending_column = column_ + cycles_this_line;
|
||||
|
||||
if(row_ >= first_sync_line && row_ < first_sync_line + 3) {
|
||||
// In effect apply an XOR to HSYNC and VSYNC flags in order to include equalising
|
||||
// pulses (and hencce keep hsync approximately where it should be during vsync).
|
||||
const int blank_start = std::max(first_sync_column - sync_length, column_);
|
||||
const int blank_end = std::min(first_sync_column, ending_column);
|
||||
if(blank_end > blank_start) {
|
||||
if(blank_start > column_) {
|
||||
crt_->output_sync(static_cast<unsigned int>(blank_start - column_) * 14);
|
||||
}
|
||||
crt_->output_blank(static_cast<unsigned int>(blank_end - blank_start) * 14);
|
||||
if(blank_end < ending_column) {
|
||||
crt_->output_sync(static_cast<unsigned int>(ending_column - blank_end) * 14);
|
||||
}
|
||||
} else {
|
||||
crt_->output_sync(static_cast<unsigned int>(cycles_this_line) * 14);
|
||||
}
|
||||
} else {
|
||||
const GraphicsMode line_mode = use_graphics_mode_ ? graphics_mode_ : GraphicsMode::Text;
|
||||
|
||||
// The first 40 columns are submitted to the CRT only upon completion;
|
||||
// they'll be either graphics or blank, depending on which side we are
|
||||
// of line 192.
|
||||
if(column_ < 40) {
|
||||
if(row_ < 192) {
|
||||
GraphicsMode pixel_mode = (!mixed_mode_ || row_ < 160) ? line_mode : GraphicsMode::Text;
|
||||
bool requires_high_density = pixel_mode != GraphicsMode::Text;
|
||||
if(!column_ || requires_high_density != pixels_are_high_density_) {
|
||||
if(column_) output_data_to_column(column_);
|
||||
pixel_pointer_ = crt_->allocate_write_area(561);
|
||||
pixel_pointer_column_ = column_;
|
||||
pixels_are_high_density_ = requires_high_density;
|
||||
graphics_carry_ = 0;
|
||||
}
|
||||
|
||||
const int pixel_end = std::min(40, ending_column);
|
||||
const int character_row = row_ >> 3;
|
||||
const int pixel_row = row_ & 7;
|
||||
const uint16_t row_address = static_cast<uint16_t>((character_row >> 3) * 40 + ((character_row&7) << 7));
|
||||
const uint16_t text_address = static_cast<uint16_t>(((video_page_+1) * 0x400) + row_address);
|
||||
|
||||
switch(pixel_mode) {
|
||||
case GraphicsMode::Text: {
|
||||
const uint8_t inverses[] = {
|
||||
0xff,
|
||||
static_cast<uint8_t>((flash_ / flash_length) * 0xff),
|
||||
0x00,
|
||||
0x00
|
||||
};
|
||||
for(int c = column_; c < pixel_end; ++c) {
|
||||
const uint8_t character = bus_handler_.perform_read(static_cast<uint16_t>(text_address + c));
|
||||
const std::size_t character_address = static_cast<std::size_t>(((character & 0x3f) << 3) + pixel_row);
|
||||
|
||||
const uint8_t character_pattern = character_rom_[character_address] ^ inverses[character >> 6];
|
||||
|
||||
// The character ROM is output MSB to LSB rather than LSB to MSB.
|
||||
pixel_pointer_[0] = character_pattern & 0x40;
|
||||
pixel_pointer_[1] = character_pattern & 0x20;
|
||||
pixel_pointer_[2] = character_pattern & 0x10;
|
||||
pixel_pointer_[3] = character_pattern & 0x08;
|
||||
pixel_pointer_[4] = character_pattern & 0x04;
|
||||
pixel_pointer_[5] = character_pattern & 0x02;
|
||||
pixel_pointer_[6] = character_pattern & 0x01;
|
||||
graphics_carry_ = character_pattern & 0x40;
|
||||
pixel_pointer_ += 7;
|
||||
}
|
||||
} break;
|
||||
|
||||
case GraphicsMode::LowRes: {
|
||||
const int row_shift = (row_&4);
|
||||
// TODO: decompose into two loops, possibly.
|
||||
for(int c = column_; c < pixel_end; ++c) {
|
||||
const uint8_t nibble = (bus_handler_.perform_read(static_cast<uint16_t>(text_address + c)) >> row_shift) & 0x0f;
|
||||
|
||||
// Low-resolution graphics mode shifts the colour code on a loop, but has to account for whether this
|
||||
// 14-sample output window is starting at the beginning of a colour cycle or halfway through.
|
||||
if(c&1) {
|
||||
pixel_pointer_[0] = pixel_pointer_[4] = pixel_pointer_[8] = pixel_pointer_[12] = nibble & 4;
|
||||
pixel_pointer_[1] = pixel_pointer_[5] = pixel_pointer_[9] = pixel_pointer_[13] = nibble & 8;
|
||||
pixel_pointer_[2] = pixel_pointer_[6] = pixel_pointer_[10] = nibble & 1;
|
||||
pixel_pointer_[3] = pixel_pointer_[7] = pixel_pointer_[11] = nibble & 2;
|
||||
graphics_carry_ = nibble & 8;
|
||||
} else {
|
||||
pixel_pointer_[0] = pixel_pointer_[4] = pixel_pointer_[8] = pixel_pointer_[12] = nibble & 1;
|
||||
pixel_pointer_[1] = pixel_pointer_[5] = pixel_pointer_[9] = pixel_pointer_[13] = nibble & 2;
|
||||
pixel_pointer_[2] = pixel_pointer_[6] = pixel_pointer_[10] = nibble & 4;
|
||||
pixel_pointer_[3] = pixel_pointer_[7] = pixel_pointer_[11] = nibble & 8;
|
||||
graphics_carry_ = nibble & 2;
|
||||
}
|
||||
pixel_pointer_ += 14;
|
||||
}
|
||||
} break;
|
||||
|
||||
case GraphicsMode::HighRes: {
|
||||
const uint16_t graphics_address = static_cast<uint16_t>(((video_page_+1) * 0x2000) + row_address + ((pixel_row&7) << 10));
|
||||
for(int c = column_; c < pixel_end; ++c) {
|
||||
const uint8_t graphic = bus_handler_.perform_read(static_cast<uint16_t>(graphics_address + c));
|
||||
|
||||
// High resolution graphics shift out LSB to MSB, optionally with a delay of half a pixel.
|
||||
// If there is a delay, the previous output level is held to bridge the gap.
|
||||
if(graphic & 0x80) {
|
||||
pixel_pointer_[0] = graphics_carry_;
|
||||
pixel_pointer_[1] = pixel_pointer_[2] = graphic & 0x01;
|
||||
pixel_pointer_[3] = pixel_pointer_[4] = graphic & 0x02;
|
||||
pixel_pointer_[5] = pixel_pointer_[6] = graphic & 0x04;
|
||||
pixel_pointer_[7] = pixel_pointer_[8] = graphic & 0x08;
|
||||
pixel_pointer_[9] = pixel_pointer_[10] = graphic & 0x10;
|
||||
pixel_pointer_[11] = pixel_pointer_[12] = graphic & 0x20;
|
||||
pixel_pointer_[13] = graphic & 0x40;
|
||||
} else {
|
||||
pixel_pointer_[0] = pixel_pointer_[1] = graphic & 0x01;
|
||||
pixel_pointer_[2] = pixel_pointer_[3] = graphic & 0x02;
|
||||
pixel_pointer_[4] = pixel_pointer_[5] = graphic & 0x04;
|
||||
pixel_pointer_[6] = pixel_pointer_[7] = graphic & 0x08;
|
||||
pixel_pointer_[8] = pixel_pointer_[9] = graphic & 0x10;
|
||||
pixel_pointer_[10] = pixel_pointer_[11] = graphic & 0x20;
|
||||
pixel_pointer_[12] = pixel_pointer_[13] = graphic & 0x40;
|
||||
}
|
||||
graphics_carry_ = graphic & 0x40;
|
||||
pixel_pointer_ += 14;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
if(ending_column >= 40) {
|
||||
output_data_to_column(40);
|
||||
}
|
||||
} else {
|
||||
if(ending_column >= 40) {
|
||||
crt_->output_blank(560);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The left border, sync, right border pattern doesn't depend on whether
|
||||
there were pixels this row and is output as soon as it is known.
|
||||
*/
|
||||
|
||||
const int first_blank_start = std::max(40, column_);
|
||||
const int first_blank_end = std::min(first_sync_column, ending_column);
|
||||
if(first_blank_end > first_blank_start) {
|
||||
crt_->output_blank(static_cast<unsigned int>(first_blank_end - first_blank_start) * 14);
|
||||
}
|
||||
|
||||
const int sync_start = std::max(first_sync_column, column_);
|
||||
const int sync_end = std::min(first_sync_column + sync_length, ending_column);
|
||||
if(sync_end > sync_start) {
|
||||
crt_->output_sync(static_cast<unsigned int>(sync_end - sync_start) * 14);
|
||||
}
|
||||
|
||||
int second_blank_start;
|
||||
if(line_mode != GraphicsMode::Text && (!mixed_mode_ || row_ < 159 || row_ >= 192)) {
|
||||
const int colour_burst_start = std::max(first_sync_column + sync_length + 1, column_);
|
||||
const int colour_burst_end = std::min(first_sync_column + sync_length + 4, ending_column);
|
||||
if(colour_burst_end > colour_burst_start) {
|
||||
crt_->output_default_colour_burst(static_cast<unsigned int>(colour_burst_end - colour_burst_start) * 14);
|
||||
}
|
||||
|
||||
second_blank_start = std::max(first_sync_column + 7, column_);
|
||||
} else {
|
||||
second_blank_start = std::max(first_sync_column + 4, column_);
|
||||
}
|
||||
|
||||
if(ending_column > second_blank_start) {
|
||||
crt_->output_blank(static_cast<unsigned int>(ending_column - second_blank_start) * 14);
|
||||
}
|
||||
}
|
||||
|
||||
int_cycles -= cycles_this_line;
|
||||
column_ = (column_ + cycles_this_line) % 65;
|
||||
if(!column_) {
|
||||
row_ = (row_ + 1) % 262;
|
||||
flash_ = (flash_ + 1) % (2 * flash_length);
|
||||
|
||||
// Add an extra half a colour cycle of blank; this isn't counted in the run_for
|
||||
// count explicitly but is promised.
|
||||
crt_->output_blank(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Obtains the last value the video read prior to time now+offset.
|
||||
*/
|
||||
uint8_t get_last_read_value(Cycles offset) {
|
||||
// Rules of generation:
|
||||
// (1) a complete sixty-five-cycle scan line consists of sixty-five consecutive bytes of
|
||||
// display buffer memory that starts twenty-five bytes prior to the actual data to be displayed.
|
||||
// (2) During VBL the data acts just as if it were starting a whole new frame from the beginning, but
|
||||
// it never finishes this pseudo-frame. After getting one third of the way through the frame (to
|
||||
// scan line $3F), it suddenly repeats the previous six scan lines ($3A through $3F) before aborting
|
||||
// to begin the next true frame.
|
||||
//
|
||||
// Source: Have an Apple Split by Bob Bishop; http://rich12345.tripod.com/aiivideo/softalk.html
|
||||
|
||||
// Determine column at offset.
|
||||
int mapped_column = column_ + offset.as_int();
|
||||
|
||||
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
||||
// (so what was column 0 is now column 25).
|
||||
mapped_column += 25;
|
||||
|
||||
// Apply carry into the row counter.
|
||||
int mapped_row = row_ + (mapped_column / 65);
|
||||
mapped_column %= 65;
|
||||
mapped_row %= 262;
|
||||
|
||||
// Apple out-of-bounds row logic.
|
||||
if(mapped_row >= 256) {
|
||||
mapped_row = 0x3a + (mapped_row&255);
|
||||
} else {
|
||||
mapped_row %= 192;
|
||||
}
|
||||
|
||||
// Calculate the address and return the value.
|
||||
uint16_t read_address = static_cast<uint16_t>(get_row_address(mapped_row) + mapped_column - 25);
|
||||
return bus_handler_.perform_read(read_address);
|
||||
}
|
||||
|
||||
private:
|
||||
uint16_t get_row_address(int row) {
|
||||
const int character_row = row >> 3;
|
||||
const int pixel_row = row & 7;
|
||||
const uint16_t row_address = static_cast<uint16_t>((character_row >> 3) * 40 + ((character_row&7) << 7));
|
||||
|
||||
GraphicsMode pixel_mode = ((!mixed_mode_ || row < 160) && use_graphics_mode_) ? graphics_mode_ : GraphicsMode::Text;
|
||||
return (pixel_mode == GraphicsMode::HighRes) ?
|
||||
static_cast<uint16_t>(((video_page_+1) * 0x2000) + row_address + ((pixel_row&7) << 10)) :
|
||||
static_cast<uint16_t>(((video_page_+1) * 0x400) + row_address);
|
||||
}
|
||||
|
||||
static const int flash_length = 8406;
|
||||
BusHandler &bus_handler_;
|
||||
void output_data_to_column(int column) {
|
||||
int length = column - pixel_pointer_column_;
|
||||
crt_->output_data(static_cast<unsigned int>(length*14), static_cast<unsigned int>(length * (pixels_are_high_density_ ? 14 : 7)));
|
||||
pixel_pointer_ = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Video_hpp */
|
||||
@@ -122,10 +122,6 @@ class ConcreteMachine:
|
||||
joysticks_.emplace_back(new Joystick(bus_.get(), 4, 1));
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
close_output();
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
return joysticks_;
|
||||
}
|
||||
@@ -157,18 +153,10 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
// to satisfy CRTMachine::Machine
|
||||
void setup_output(float aspect_ratio) override {
|
||||
bus_->tia_.reset(new TIA);
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
bus_->speaker_.set_input_rate(static_cast<float>(get_clock_rate() / static_cast<double>(CPUTicksPerAudioTick)));
|
||||
bus_->tia_->get_crt()->set_delegate(this);
|
||||
}
|
||||
|
||||
void close_output() override {
|
||||
bus_.reset();
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *get_crt() override {
|
||||
return bus_->tia_->get_crt();
|
||||
bus_->tia_.set_crt_delegate(this);
|
||||
bus_->tia_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
@@ -181,15 +169,15 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
// to satisfy Outputs::CRT::Delegate
|
||||
void crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, unsigned int number_of_frames, unsigned int number_of_unexpected_vertical_syncs) override {
|
||||
void crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, int number_of_frames, int number_of_unexpected_vertical_syncs) override {
|
||||
const std::size_t number_of_frame_records = sizeof(frame_records_) / sizeof(frame_records_[0]);
|
||||
frame_records_[frame_record_pointer_ % number_of_frame_records].number_of_frames = number_of_frames;
|
||||
frame_records_[frame_record_pointer_ % number_of_frame_records].number_of_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs;
|
||||
frame_record_pointer_ ++;
|
||||
|
||||
if(frame_record_pointer_ >= 6) {
|
||||
unsigned int total_number_of_frames = 0;
|
||||
unsigned int total_number_of_unexpected_vertical_syncs = 0;
|
||||
int total_number_of_frames = 0;
|
||||
int total_number_of_unexpected_vertical_syncs = 0;
|
||||
for(std::size_t c = 0; c < number_of_frame_records; c++) {
|
||||
total_number_of_frames += frame_records_[c].number_of_frames;
|
||||
total_number_of_unexpected_vertical_syncs += frame_records_[c].number_of_unexpected_vertical_syncs;
|
||||
@@ -205,10 +193,10 @@ class ConcreteMachine:
|
||||
double clock_rate;
|
||||
if(is_ntsc_) {
|
||||
clock_rate = NTSC_clock_rate;
|
||||
bus_->tia_->set_output_mode(TIA::OutputMode::NTSC);
|
||||
bus_->tia_.set_output_mode(TIA::OutputMode::NTSC);
|
||||
} else {
|
||||
clock_rate = PAL_clock_rate;
|
||||
bus_->tia_->set_output_mode(TIA::OutputMode::PAL);
|
||||
bus_->tia_.set_output_mode(TIA::OutputMode::PAL);
|
||||
}
|
||||
|
||||
bus_->speaker_.set_input_rate(static_cast<float>(clock_rate / static_cast<double>(CPUTicksPerAudioTick)));
|
||||
@@ -228,10 +216,8 @@ class ConcreteMachine:
|
||||
|
||||
// output frame rate tracker
|
||||
struct FrameRecord {
|
||||
unsigned int number_of_frames;
|
||||
unsigned int number_of_unexpected_vertical_syncs;
|
||||
|
||||
FrameRecord() : number_of_frames(0), number_of_unexpected_vertical_syncs(0) {}
|
||||
int number_of_frames = 0;
|
||||
int number_of_unexpected_vertical_syncs = 0;
|
||||
} frame_records_[4];
|
||||
unsigned int frame_record_pointer_ = 0;
|
||||
bool is_ntsc_ = true;
|
||||
|
||||
@@ -36,7 +36,7 @@ class Bus {
|
||||
|
||||
// the RIOT, TIA and speaker
|
||||
PIA mos6532_;
|
||||
std::shared_ptr<TIA> tia_;
|
||||
TIA tia_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TIASound tia_sound_;
|
||||
@@ -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>());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ template<class T> class Cartridge:
|
||||
// effect until the next read; therefore it isn't safe to assume that signalling ready immediately
|
||||
// skips to the end of the line.
|
||||
if(operation == CPU::MOS6502::BusOperation::Ready)
|
||||
cycles_run_for = tia_->get_cycles_until_horizontal_blank(cycles_since_video_update_);
|
||||
cycles_run_for = tia_.get_cycles_until_horizontal_blank(cycles_since_video_update_);
|
||||
|
||||
cycles_since_speaker_update_ += Cycles(cycles_run_for);
|
||||
cycles_since_video_update_ += Cycles(cycles_run_for);
|
||||
@@ -101,7 +101,7 @@ template<class T> class Cartridge:
|
||||
case 0x05: // missile 1 / playfield / ball collisions
|
||||
case 0x06: // ball / playfield collisions
|
||||
case 0x07: // player / player, missile / missile collisions
|
||||
returnValue &= tia_->get_collision_flags(decodedAddress);
|
||||
returnValue &= tia_.get_collision_flags(decodedAddress);
|
||||
break;
|
||||
|
||||
case 0x08:
|
||||
@@ -120,52 +120,52 @@ template<class T> class Cartridge:
|
||||
} else {
|
||||
const uint16_t decodedAddress = address & 0x3f;
|
||||
switch(decodedAddress) {
|
||||
case 0x00: update_video(); tia_->set_sync(*value & 0x02); break;
|
||||
case 0x01: update_video(); tia_->set_blank(*value & 0x02); break;
|
||||
case 0x00: update_video(); tia_.set_sync(*value & 0x02); break;
|
||||
case 0x01: update_video(); tia_.set_blank(*value & 0x02); break;
|
||||
|
||||
case 0x02: m6502_.set_ready_line(true); break;
|
||||
case 0x03:
|
||||
update_video();
|
||||
tia_->reset_horizontal_counter();
|
||||
tia_.reset_horizontal_counter();
|
||||
horizontal_counter_resets_++;
|
||||
break;
|
||||
// TODO: audio will now be out of synchronisation. Fix.
|
||||
|
||||
case 0x04:
|
||||
case 0x05: update_video(); tia_->set_player_number_and_size(decodedAddress - 0x04, *value); break;
|
||||
case 0x05: update_video(); tia_.set_player_number_and_size(decodedAddress - 0x04, *value); break;
|
||||
case 0x06:
|
||||
case 0x07: update_video(); tia_->set_player_missile_colour(decodedAddress - 0x06, *value); break;
|
||||
case 0x08: update_video(); tia_->set_playfield_ball_colour(*value); break;
|
||||
case 0x09: update_video(); tia_->set_background_colour(*value); break;
|
||||
case 0x0a: update_video(); tia_->set_playfield_control_and_ball_size(*value); break;
|
||||
case 0x07: update_video(); tia_.set_player_missile_colour(decodedAddress - 0x06, *value); break;
|
||||
case 0x08: update_video(); tia_.set_playfield_ball_colour(*value); break;
|
||||
case 0x09: update_video(); tia_.set_background_colour(*value); break;
|
||||
case 0x0a: update_video(); tia_.set_playfield_control_and_ball_size(*value); break;
|
||||
case 0x0b:
|
||||
case 0x0c: update_video(); tia_->set_player_reflected(decodedAddress - 0x0b, !((*value)&8)); break;
|
||||
case 0x0c: update_video(); tia_.set_player_reflected(decodedAddress - 0x0b, !((*value)&8)); break;
|
||||
case 0x0d:
|
||||
case 0x0e:
|
||||
case 0x0f: update_video(); tia_->set_playfield(decodedAddress - 0x0d, *value); break;
|
||||
case 0x0f: update_video(); tia_.set_playfield(decodedAddress - 0x0d, *value); break;
|
||||
case 0x10:
|
||||
case 0x11: update_video(); tia_->set_player_position(decodedAddress - 0x10); break;
|
||||
case 0x11: update_video(); tia_.set_player_position(decodedAddress - 0x10); break;
|
||||
case 0x12:
|
||||
case 0x13: update_video(); tia_->set_missile_position(decodedAddress - 0x12); break;
|
||||
case 0x14: update_video(); tia_->set_ball_position(); break;
|
||||
case 0x13: update_video(); tia_.set_missile_position(decodedAddress - 0x12); break;
|
||||
case 0x14: update_video(); tia_.set_ball_position(); break;
|
||||
case 0x1b:
|
||||
case 0x1c: update_video(); tia_->set_player_graphic(decodedAddress - 0x1b, *value); break;
|
||||
case 0x1c: update_video(); tia_.set_player_graphic(decodedAddress - 0x1b, *value); break;
|
||||
case 0x1d:
|
||||
case 0x1e: update_video(); tia_->set_missile_enable(decodedAddress - 0x1d, (*value)&2); break;
|
||||
case 0x1f: update_video(); tia_->set_ball_enable((*value)&2); break;
|
||||
case 0x1e: update_video(); tia_.set_missile_enable(decodedAddress - 0x1d, (*value)&2); break;
|
||||
case 0x1f: update_video(); tia_.set_ball_enable((*value)&2); break;
|
||||
case 0x20:
|
||||
case 0x21: update_video(); tia_->set_player_motion(decodedAddress - 0x20, *value); break;
|
||||
case 0x21: update_video(); tia_.set_player_motion(decodedAddress - 0x20, *value); break;
|
||||
case 0x22:
|
||||
case 0x23: update_video(); tia_->set_missile_motion(decodedAddress - 0x22, *value); break;
|
||||
case 0x24: update_video(); tia_->set_ball_motion(*value); break;
|
||||
case 0x23: update_video(); tia_.set_missile_motion(decodedAddress - 0x22, *value); break;
|
||||
case 0x24: update_video(); tia_.set_ball_motion(*value); break;
|
||||
case 0x25:
|
||||
case 0x26: tia_->set_player_delay(decodedAddress - 0x25, (*value)&1); break;
|
||||
case 0x27: tia_->set_ball_delay((*value)&1); break;
|
||||
case 0x26: tia_.set_player_delay(decodedAddress - 0x25, (*value)&1); break;
|
||||
case 0x27: tia_.set_ball_delay((*value)&1); break;
|
||||
case 0x28:
|
||||
case 0x29: update_video(); tia_->set_missile_position_to_player(decodedAddress - 0x28, (*value)&2); break;
|
||||
case 0x2a: update_video(); tia_->move(); break;
|
||||
case 0x2b: update_video(); tia_->clear_motion(); break;
|
||||
case 0x2c: update_video(); tia_->clear_collision_flags(); break;
|
||||
case 0x29: update_video(); tia_.set_missile_position_to_player(decodedAddress - 0x28, (*value)&2); break;
|
||||
case 0x2a: update_video(); tia_.move(); break;
|
||||
case 0x2b: update_video(); tia_.clear_motion(); break;
|
||||
case 0x2c: update_video(); tia_.clear_collision_flags(); break;
|
||||
|
||||
case 0x15:
|
||||
case 0x16: update_audio(); tia_sound_.set_control(decodedAddress - 0x15, *value); break;
|
||||
@@ -192,7 +192,7 @@ template<class T> class Cartridge:
|
||||
}
|
||||
}
|
||||
|
||||
if(!tia_->get_cycles_until_horizontal_blank(cycles_since_video_update_)) m6502_.set_ready_line(false);
|
||||
if(!tia_.get_cycles_until_horizontal_blank(cycles_since_video_update_)) m6502_.set_ready_line(false);
|
||||
|
||||
return Cycles(cycles_run_for / 3);
|
||||
}
|
||||
@@ -204,7 +204,7 @@ template<class T> class Cartridge:
|
||||
}
|
||||
|
||||
protected:
|
||||
CPU::MOS6502::Processor<Cartridge<T>, true> m6502_;
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, Cartridge<T>, true> m6502_;
|
||||
std::vector<uint8_t> rom_;
|
||||
|
||||
private:
|
||||
|
||||
@@ -22,12 +22,10 @@ namespace {
|
||||
uint8_t reverse_table[256];
|
||||
}
|
||||
|
||||
TIA::TIA(bool create_crt) {
|
||||
if(create_crt) {
|
||||
crt_.reset(new Outputs::CRT::CRT(cycles_per_line * 2 - 1, 1, Outputs::CRT::DisplayType::NTSC60, 1));
|
||||
crt_->set_video_signal(Outputs::CRT::VideoSignal::Composite);
|
||||
set_output_mode(OutputMode::NTSC);
|
||||
}
|
||||
TIA::TIA():
|
||||
crt_(cycles_per_line * 2 - 1, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8) {
|
||||
|
||||
set_output_mode(OutputMode::NTSC);
|
||||
|
||||
for(int c = 0; c < 256; c++) {
|
||||
reverse_table[c] = static_cast<uint8_t>(
|
||||
@@ -113,51 +111,35 @@ TIA::TIA(bool create_crt) {
|
||||
}
|
||||
}
|
||||
|
||||
TIA::TIA() : TIA(true) {}
|
||||
|
||||
TIA::TIA(std::function<void(uint8_t *output_buffer)> line_end_function) : TIA(false) {
|
||||
line_end_function_ = line_end_function;
|
||||
}
|
||||
|
||||
void TIA::set_output_mode(Atari2600::TIA::OutputMode output_mode) {
|
||||
Outputs::CRT::DisplayType display_type;
|
||||
Outputs::Display::Type display_type;
|
||||
tv_standard_ = output_mode;
|
||||
|
||||
if(output_mode == OutputMode::NTSC) {
|
||||
crt_->set_svideo_sampling_function(
|
||||
"vec2 svideo_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
|
||||
"{"
|
||||
"uint c = texture(texID, coordinate).r;"
|
||||
"uint y = c & 14u;"
|
||||
"uint iPhase = (c >> 4);"
|
||||
|
||||
"float phaseOffset = 6.283185308 * float(iPhase) / 13.0 + 5.074880441076923;"
|
||||
"return vec2(float(y) / 14.0, step(1, iPhase) * cos(phase - phaseOffset));"
|
||||
"}");
|
||||
display_type = Outputs::CRT::DisplayType::NTSC60;
|
||||
display_type = Outputs::Display::Type::NTSC60;
|
||||
} else {
|
||||
crt_->set_svideo_sampling_function(
|
||||
"vec2 svideo_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
|
||||
"{"
|
||||
"uint c = texture(texID, coordinate).r;"
|
||||
"uint y = c & 14u;"
|
||||
"uint iPhase = (c >> 4);"
|
||||
|
||||
"uint direction = iPhase & 1u;"
|
||||
"float phaseOffset = float(7u - direction) + (float(direction) - 0.5) * 2.0 * float(iPhase >> 1);"
|
||||
"phaseOffset *= 6.283185308 / 12.0;"
|
||||
"return vec2(float(y) / 14.0, step(4, (iPhase + 2u) & 15u) * cos(phase + phaseOffset));"
|
||||
"}");
|
||||
display_type = Outputs::CRT::DisplayType::PAL50;
|
||||
display_type = Outputs::Display::Type::PAL50;
|
||||
}
|
||||
crt_->set_video_signal(Outputs::CRT::VideoSignal::Composite);
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::CompositeColour);
|
||||
|
||||
// line number of cycles in a line of video is one less than twice the number of clock cycles per line; the Atari
|
||||
// outputs 228 colour cycles of material per line when an NTSC line 227.5. Since all clock numbers will be doubled
|
||||
// later, cycles_per_line * 2 - 1 is therefore the real length of an NTSC line, even though we're going to supply
|
||||
// cycles_per_line * 2 cycles of information from one sync edge to the next
|
||||
crt_->set_new_display_type(cycles_per_line * 2 - 1, display_type);
|
||||
crt_.set_new_display_type(cycles_per_line * 2 - 1, display_type);
|
||||
|
||||
/* speaker_->set_input_rate(static_cast<float>(get_clock_rate() / 38.0));*/
|
||||
// Update the luminance/phase mappings of the current palette.
|
||||
for(size_t c = 0; c < colour_palette_.size(); ++c) {
|
||||
set_colour_palette_entry(c, colour_palette_[c].original);
|
||||
}
|
||||
}
|
||||
|
||||
void TIA::set_crt_delegate(Outputs::CRT::Delegate *delegate) {
|
||||
crt_.set_delegate(delegate);
|
||||
}
|
||||
|
||||
void TIA::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void TIA::run_for(const Cycles cycles) {
|
||||
@@ -198,7 +180,40 @@ int TIA::get_cycles_until_horizontal_blank(const Cycles from_offset) {
|
||||
}
|
||||
|
||||
void TIA::set_background_colour(uint8_t colour) {
|
||||
colour_palette_[static_cast<int>(ColourIndex::Background)] = colour;
|
||||
set_colour_palette_entry(size_t(ColourIndex::Background), colour);
|
||||
}
|
||||
|
||||
void TIA::set_colour_palette_entry(size_t index, uint8_t colour) {
|
||||
const uint8_t luminance = ((colour & 14) * 255) / 14;
|
||||
|
||||
uint8_t phase = colour >> 4;
|
||||
|
||||
if(tv_standard_ == OutputMode::NTSC) {
|
||||
if(!phase) phase = 255;
|
||||
else {
|
||||
phase = -(phase * 127) / 13;
|
||||
phase -= 102;
|
||||
phase &= 127;
|
||||
}
|
||||
} else {
|
||||
if(phase < 2 || phase > 13) {
|
||||
phase = 255;
|
||||
} else {
|
||||
const auto direction = phase & 1;
|
||||
|
||||
phase >>= 1;
|
||||
if(direction) phase ^= 0xf;
|
||||
phase = (phase + 6 + direction) & 0xf;
|
||||
|
||||
phase = (phase * 127) / 12;
|
||||
phase &= 127;
|
||||
}
|
||||
}
|
||||
|
||||
colour_palette_[index].original = colour;
|
||||
uint8_t *target = reinterpret_cast<uint8_t *>(&colour_palette_[index].luminance_phase);
|
||||
target[0] = luminance;
|
||||
target[1] = phase;
|
||||
}
|
||||
|
||||
void TIA::set_playfield(uint16_t offset, uint8_t value) {
|
||||
@@ -238,7 +253,7 @@ void TIA::set_playfield_control_and_ball_size(uint8_t value) {
|
||||
}
|
||||
|
||||
void TIA::set_playfield_ball_colour(uint8_t colour) {
|
||||
colour_palette_[static_cast<int>(ColourIndex::PlayfieldBall)] = colour;
|
||||
set_colour_palette_entry(size_t(ColourIndex::PlayfieldBall), colour);
|
||||
}
|
||||
|
||||
void TIA::set_player_number_and_size(int player, uint8_t value) {
|
||||
@@ -300,7 +315,7 @@ void TIA::set_player_motion(int player, uint8_t motion) {
|
||||
|
||||
void TIA::set_player_missile_colour(int player, uint8_t colour) {
|
||||
assert(player >= 0 && player < 2);
|
||||
colour_palette_[static_cast<int>(ColourIndex::PlayerMissile0) + player] = colour;
|
||||
set_colour_palette_entry(size_t(ColourIndex::PlayerMissile0) + size_t(player), colour);
|
||||
}
|
||||
|
||||
void TIA::set_missile_enable(int missile, bool enabled) {
|
||||
@@ -382,7 +397,6 @@ void TIA::output_for_cycles(int number_of_cycles) {
|
||||
bool is_reset = output_cursor < 224 && horizontal_counter_ >= 224;
|
||||
|
||||
if(!output_cursor) {
|
||||
if(line_end_function_) line_end_function_(collision_buffer_);
|
||||
std::memset(collision_buffer_, 0, sizeof(collision_buffer_));
|
||||
|
||||
ball_.motion_time %= 228;
|
||||
@@ -407,11 +421,11 @@ void TIA::output_for_cycles(int number_of_cycles) {
|
||||
#define Period(function, target) \
|
||||
if(output_cursor < target) { \
|
||||
if(horizontal_counter_ <= target) { \
|
||||
if(crt_) crt_->function(static_cast<unsigned int>((horizontal_counter_ - output_cursor) * 2)); \
|
||||
crt_.function((horizontal_counter_ - output_cursor) * 2); \
|
||||
horizontal_counter_ %= cycles_per_line; \
|
||||
return; \
|
||||
} else { \
|
||||
if(crt_) crt_->function(static_cast<unsigned int>((target - output_cursor) * 2)); \
|
||||
crt_.function((target - output_cursor) * 2); \
|
||||
output_cursor = target; \
|
||||
} \
|
||||
}
|
||||
@@ -437,19 +451,17 @@ void TIA::output_for_cycles(int number_of_cycles) {
|
||||
if(output_mode_ & blank_flag) {
|
||||
if(pixel_target_) {
|
||||
output_pixels(pixels_start_location_, output_cursor);
|
||||
if(crt_) {
|
||||
const unsigned int data_length = static_cast<unsigned int>(output_cursor - pixels_start_location_);
|
||||
crt_->output_data(data_length * 2, data_length);
|
||||
}
|
||||
const int data_length = int(output_cursor - pixels_start_location_);
|
||||
crt_.output_data(data_length * 2, size_t(data_length));
|
||||
pixel_target_ = nullptr;
|
||||
pixels_start_location_ = 0;
|
||||
}
|
||||
int duration = std::min(228, horizontal_counter_) - output_cursor;
|
||||
if(crt_) crt_->output_blank(static_cast<unsigned int>(duration * 2));
|
||||
crt_.output_blank(duration * 2);
|
||||
} else {
|
||||
if(!pixels_start_location_ && crt_) {
|
||||
if(!pixels_start_location_) {
|
||||
pixels_start_location_ = output_cursor;
|
||||
pixel_target_ = crt_->allocate_write_area(160);
|
||||
pixel_target_ = reinterpret_cast<uint16_t *>(crt_.begin_data(160));
|
||||
}
|
||||
|
||||
// convert that into pixels
|
||||
@@ -461,9 +473,9 @@ void TIA::output_for_cycles(int number_of_cycles) {
|
||||
output_cursor++;
|
||||
}
|
||||
|
||||
if(horizontal_counter_ == cycles_per_line && crt_) {
|
||||
const unsigned int data_length = static_cast<unsigned int>(output_cursor - pixels_start_location_);
|
||||
crt_->output_data(data_length * 2, data_length);
|
||||
if(horizontal_counter_ == cycles_per_line) {
|
||||
const int data_length = int(output_cursor - pixels_start_location_);
|
||||
crt_.output_data(data_length * 2, size_t(data_length));
|
||||
pixel_target_ = nullptr;
|
||||
pixels_start_location_ = 0;
|
||||
}
|
||||
@@ -480,7 +492,7 @@ void TIA::output_pixels(int start, int end) {
|
||||
|
||||
if(start < first_pixel_cycle+8 && horizontal_blank_extend_) {
|
||||
while(start < end && start < first_pixel_cycle+8) {
|
||||
pixel_target_[target_position] = 0;
|
||||
pixel_target_[target_position] = 0xff00; // TODO: this assumes little endianness.
|
||||
start++;
|
||||
target_position++;
|
||||
}
|
||||
@@ -489,13 +501,13 @@ void TIA::output_pixels(int start, int end) {
|
||||
if(playfield_priority_ == PlayfieldPriority::Score) {
|
||||
while(start < end && start < first_pixel_cycle + 80) {
|
||||
uint8_t buffer_value = collision_buffer_[start - first_pixel_cycle];
|
||||
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[static_cast<int>(ColourMode::ScoreLeft)][buffer_value]];
|
||||
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[static_cast<int>(ColourMode::ScoreLeft)][buffer_value]].luminance_phase;
|
||||
start++;
|
||||
target_position++;
|
||||
}
|
||||
while(start < end) {
|
||||
uint8_t buffer_value = collision_buffer_[start - first_pixel_cycle];
|
||||
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[static_cast<int>(ColourMode::ScoreRight)][buffer_value]];
|
||||
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[static_cast<int>(ColourMode::ScoreRight)][buffer_value]].luminance_phase;
|
||||
start++;
|
||||
target_position++;
|
||||
}
|
||||
@@ -503,7 +515,7 @@ void TIA::output_pixels(int start, int end) {
|
||||
int table_index = static_cast<int>((playfield_priority_ == PlayfieldPriority::Standard) ? ColourMode::Standard : ColourMode::OnTop);
|
||||
while(start < end) {
|
||||
uint8_t buffer_value = collision_buffer_[start - first_pixel_cycle];
|
||||
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[table_index][buffer_value]];
|
||||
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[table_index][buffer_value]].luminance_phase;
|
||||
start++;
|
||||
target_position++;
|
||||
}
|
||||
@@ -518,20 +530,16 @@ void TIA::output_line() {
|
||||
break;
|
||||
case sync_flag:
|
||||
case sync_flag | blank_flag:
|
||||
if(crt_) {
|
||||
crt_->output_sync(32);
|
||||
crt_->output_blank(32);
|
||||
crt_->output_sync(392);
|
||||
}
|
||||
crt_.output_sync(32);
|
||||
crt_.output_blank(32);
|
||||
crt_.output_sync(392);
|
||||
horizontal_blank_extend_ = false;
|
||||
break;
|
||||
case blank_flag:
|
||||
if(crt_) {
|
||||
crt_->output_blank(32);
|
||||
crt_->output_sync(32);
|
||||
crt_->output_default_colour_burst(32);
|
||||
crt_->output_blank(360);
|
||||
}
|
||||
crt_.output_blank(32);
|
||||
crt_.output_sync(32);
|
||||
crt_.output_default_colour_burst(32);
|
||||
crt_.output_blank(360);
|
||||
horizontal_blank_extend_ = false;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -9,19 +9,19 @@
|
||||
#ifndef TIA_hpp
|
||||
#define TIA_hpp
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#include "../CRTMachine.hpp"
|
||||
#include "../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
namespace Atari2600 {
|
||||
|
||||
class TIA {
|
||||
public:
|
||||
TIA();
|
||||
// The supplied hook is for unit testing only; if instantiated with a line_end_function then it will
|
||||
// be called with the latest collision buffer upon the conclusion of each line. What's a collision
|
||||
// buffer? It's an implementation detail. If you're not writing a unit test, leave it alone.
|
||||
TIA(std::function<void(uint8_t *output_buffer)> line_end_function);
|
||||
|
||||
enum class OutputMode {
|
||||
NTSC, PAL
|
||||
@@ -35,7 +35,7 @@ class TIA {
|
||||
|
||||
void set_sync(bool sync);
|
||||
void set_blank(bool blank);
|
||||
void reset_horizontal_counter(); // Reset is delayed by four cycles.
|
||||
void reset_horizontal_counter(); // Reset is delayed by four cycles.
|
||||
|
||||
/*!
|
||||
@returns the number of cycles between (current TIA time) + from_offset to the current or
|
||||
@@ -73,12 +73,11 @@ class TIA {
|
||||
uint8_t get_collision_flags(int offset);
|
||||
void clear_collision_flags();
|
||||
|
||||
Outputs::CRT::CRT *get_crt() { return crt_.get(); }
|
||||
void set_crt_delegate(Outputs::CRT::Delegate *);
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
|
||||
private:
|
||||
TIA(bool create_crt);
|
||||
std::unique_ptr<Outputs::CRT::CRT> crt_;
|
||||
std::function<void(uint8_t *output_buffer)> line_end_function_;
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
// the master counter; counts from 0 to 228 with all visible pixels being in the final 160
|
||||
int horizontal_counter_ = 0;
|
||||
@@ -107,7 +106,7 @@ class TIA {
|
||||
ScoreRight,
|
||||
OnTop
|
||||
};
|
||||
uint8_t colour_mask_by_mode_collision_flags_[4][64]; // maps from [ColourMode][CollisionMark] to colour_pallete_ entry
|
||||
uint8_t colour_mask_by_mode_collision_flags_[4][64]; // maps from [ColourMode][CollisionMark] to colour_palette_ entry
|
||||
|
||||
enum class ColourIndex {
|
||||
Background = 0,
|
||||
@@ -115,7 +114,13 @@ class TIA {
|
||||
PlayerMissile0,
|
||||
PlayerMissile1
|
||||
};
|
||||
uint8_t colour_palette_[4];
|
||||
struct Colour {
|
||||
uint16_t luminance_phase;
|
||||
uint8_t original;
|
||||
};
|
||||
std::array<Colour, 4> colour_palette_;
|
||||
void set_colour_palette_entry(size_t index, uint8_t colour);
|
||||
OutputMode tv_standard_;
|
||||
|
||||
// playfield state
|
||||
int background_half_mask_ = 0;
|
||||
@@ -302,7 +307,7 @@ class TIA {
|
||||
inline void output_line();
|
||||
|
||||
int pixels_start_location_ = 0;
|
||||
uint8_t *pixel_target_ = nullptr;
|
||||
uint16_t *pixel_target_ = nullptr;
|
||||
inline void output_pixels(int start, int end);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#ifndef CRTMachine_hpp
|
||||
#define CRTMachine_hpp
|
||||
|
||||
#include "../Outputs/CRT/CRT.hpp"
|
||||
#include "../Outputs/ScanTarget.hpp"
|
||||
#include "../Outputs/Speaker/Speaker.hpp"
|
||||
#include "../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../ClockReceiver/TimeTypes.hpp"
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <cmath>
|
||||
|
||||
// TODO: rename.
|
||||
namespace CRTMachine {
|
||||
|
||||
/*!
|
||||
@@ -29,26 +30,19 @@ namespace CRTMachine {
|
||||
class Machine {
|
||||
public:
|
||||
/*!
|
||||
Causes the machine to set up its CRT and, if it has one, speaker. The caller guarantees
|
||||
that an OpenGL context is bound.
|
||||
*/
|
||||
virtual void setup_output(float aspect_ratio) = 0;
|
||||
Causes the machine to set up its display and, if it has one, speaker.
|
||||
|
||||
/*!
|
||||
Gives the machine a chance to release all owned resources. The caller guarantees that the
|
||||
OpenGL context is bound.
|
||||
The @c scan_target will receive all video output; the caller guarantees
|
||||
that it is non-null.
|
||||
*/
|
||||
virtual void close_output() = 0;
|
||||
|
||||
/// @returns The CRT this machine is drawing to. Should not be @c nullptr.
|
||||
virtual Outputs::CRT::CRT *get_crt() = 0;
|
||||
virtual void set_scan_target(Outputs::Display::ScanTarget *scan_target) = 0;
|
||||
|
||||
/// @returns The speaker that receives this machine's output, or @c nullptr if this machine is mute.
|
||||
virtual Outputs::Speaker::Speaker *get_speaker() = 0;
|
||||
|
||||
/// @returns The confidence that this machine is running content it understands.
|
||||
virtual float get_confidence() { return 0.5f; }
|
||||
virtual void print_type() {}
|
||||
virtual std::string debug_type() { return ""; }
|
||||
|
||||
/// Runs the machine for @c duration seconds.
|
||||
virtual void run_for(Time::Seconds duration) {
|
||||
@@ -68,32 +62,33 @@ class Machine {
|
||||
}
|
||||
|
||||
/*!
|
||||
Maps from Configurable::Display to Outputs::CRT::VideoSignal and calls
|
||||
@c set_video_signal with the result.
|
||||
Maps from Configurable::Display to Outputs::Display::VideoSignal and calls
|
||||
@c set_display_type with the result.
|
||||
*/
|
||||
void set_video_signal_configurable(Configurable::Display type) {
|
||||
Outputs::CRT::VideoSignal signal;
|
||||
Outputs::Display::DisplayType display_type;
|
||||
switch(type) {
|
||||
default:
|
||||
case Configurable::Display::RGB:
|
||||
signal = Outputs::CRT::VideoSignal::RGB;
|
||||
display_type = Outputs::Display::DisplayType::RGB;
|
||||
break;
|
||||
case Configurable::Display::SVideo:
|
||||
signal = Outputs::CRT::VideoSignal::SVideo;
|
||||
display_type = Outputs::Display::DisplayType::SVideo;
|
||||
break;
|
||||
case Configurable::Display::Composite:
|
||||
signal = Outputs::CRT::VideoSignal::Composite;
|
||||
case Configurable::Display::CompositeColour:
|
||||
display_type = Outputs::Display::DisplayType::CompositeColour;
|
||||
break;
|
||||
case Configurable::Display::CompositeMonochrome:
|
||||
display_type = Outputs::Display::DisplayType::CompositeMonochrome;
|
||||
break;
|
||||
}
|
||||
set_video_signal(signal);
|
||||
set_display_type(display_type);
|
||||
}
|
||||
|
||||
/*!
|
||||
Forwards the video signal to the CRT returned by get_crt().
|
||||
Forwards the video signal to the target returned by get_crt().
|
||||
*/
|
||||
virtual void set_video_signal(Outputs::CRT::VideoSignal video_signal) {
|
||||
get_crt()->set_video_signal(video_signal);
|
||||
}
|
||||
virtual void set_display_type(Outputs::Display::DisplayType display_type) {}
|
||||
|
||||
private:
|
||||
double clock_rate_ = 1.0;
|
||||
|
||||
@@ -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() :
|
||||
@@ -76,7 +84,7 @@ class Joystick: public Inputs::ConcreteJoystick {
|
||||
}
|
||||
break;
|
||||
|
||||
case Input::Up: if(is_active) direction_ &= ~0x01; else direction_ |= 0x01; break;
|
||||
case Input::Up: if(is_active) direction_ &= ~0x01; else direction_ |= 0x01; break;
|
||||
case Input::Right: if(is_active) direction_ &= ~0x02; else direction_ |= 0x02; break;
|
||||
case Input::Down: if(is_active) direction_ &= ~0x04; else direction_ |= 0x04; break;
|
||||
case Input::Left: if(is_active) direction_ &= ~0x08; else direction_ |= 0x08; break;
|
||||
@@ -100,18 +108,20 @@ class Joystick: public Inputs::ConcreteJoystick {
|
||||
|
||||
private:
|
||||
uint8_t direction_ = 0xff;
|
||||
uint8_t keypad_ = 0xff;
|
||||
uint8_t keypad_ = 0x7f;
|
||||
};
|
||||
|
||||
class ConcreteMachine:
|
||||
public Machine,
|
||||
public CPU::Z80::BusHandler,
|
||||
public CRTMachine::Machine,
|
||||
public Configurable::Device,
|
||||
public JoystickMachine::Machine {
|
||||
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
z80_(*this),
|
||||
vdp_(TI::TMS::TMS9918A),
|
||||
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, sn76489_divider),
|
||||
ay_(audio_queue_),
|
||||
mixer_(sn76489_, ay_),
|
||||
@@ -122,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;
|
||||
@@ -141,15 +150,27 @@ class ConcreteMachine:
|
||||
cartridge_address_limit_ = static_cast<uint16_t>(0x8000 + cartridge_.size() - 1);
|
||||
|
||||
if(cartridge_.size() > 32768) {
|
||||
// Ensure the cartrige is a multiple of 16kb in size, as that won't
|
||||
// be checked when paging.
|
||||
const size_t extension = (16384 - (cartridge_.size() & 16383)) % 16384;
|
||||
cartridge_.resize(cartridge_.size() + extension);
|
||||
|
||||
cartridge_pages_[0] = &cartridge_[cartridge_.size() - 16384];
|
||||
cartridge_pages_[1] = cartridge_.data();
|
||||
is_megacart_ = true;
|
||||
} else {
|
||||
// Ensure at least 32kb is allocated to the cartrige so that
|
||||
// reads are never out of bounds.
|
||||
cartridge_.resize(32768);
|
||||
|
||||
cartridge_pages_[0] = cartridge_.data();
|
||||
cartridge_pages_[1] = cartridge_.data() + 16384;
|
||||
is_megacart_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ColecoVisions have composite output only.
|
||||
vdp_->set_display_type(Outputs::Display::DisplayType::CompositeColour);
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
@@ -160,17 +181,12 @@ class ConcreteMachine:
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
void setup_output(float aspect_ratio) override {
|
||||
vdp_.reset(new TI::TMS9918(TI::TMS9918::TMS9918A));
|
||||
get_crt()->set_video_signal(Outputs::CRT::VideoSignal::Composite);
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
vdp_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void close_output() override {
|
||||
vdp_.reset();
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *get_crt() override {
|
||||
return vdp_->get_crt();
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
vdp_->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
@@ -183,145 +199,147 @@ class ConcreteMachine:
|
||||
|
||||
// MARK: Z80::BusHandler
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
// The SN76489 will use its ready line to trigger the Z80's wait for three
|
||||
// cycles when accessed. Everything else runs at full speed. Short-circuit
|
||||
// that whole piece of communications by just accruing the time here if applicable.
|
||||
const HalfCycles penalty(
|
||||
(
|
||||
cycle.operation == CPU::Z80::PartialMachineCycle::Output &&
|
||||
((*cycle.address >> 5) & 7) == 7
|
||||
) ? 6 : 0
|
||||
);
|
||||
// The SN76489 will use its ready line to trigger the Z80's wait, which will add
|
||||
// thirty-one (!) cycles when accessed. M1 cycles are extended by a single cycle.
|
||||
// This code works out the delay up front in order to simplify execution flow, though
|
||||
// technically this is a little duplicative.
|
||||
HalfCycles penalty(0);
|
||||
if(cycle.operation == CPU::Z80::PartialMachineCycle::Output && ((*cycle.address >> 5) & 7) == 7) {
|
||||
penalty = HalfCycles(62);
|
||||
} else if(cycle.operation == CPU::Z80::PartialMachineCycle::ReadOpcode) {
|
||||
penalty = HalfCycles(2);
|
||||
}
|
||||
const HalfCycles length = cycle.length + penalty;
|
||||
|
||||
time_since_vdp_update_ += length;
|
||||
vdp_ += length;
|
||||
time_since_sn76489_update_ += length;
|
||||
|
||||
uint16_t address = cycle.address ? *cycle.address : 0x0000;
|
||||
switch(cycle.operation) {
|
||||
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
||||
if(!address) pc_zero_accesses_++;
|
||||
case CPU::Z80::PartialMachineCycle::Read:
|
||||
if(address < 0x2000) {
|
||||
if(super_game_module_.replace_bios) {
|
||||
// Act only if necessary.
|
||||
if(cycle.is_terminal()) {
|
||||
uint16_t address = cycle.address ? *cycle.address : 0x0000;
|
||||
switch(cycle.operation) {
|
||||
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
||||
if(!address) pc_zero_accesses_++;
|
||||
case CPU::Z80::PartialMachineCycle::Read:
|
||||
if(address < 0x2000) {
|
||||
if(super_game_module_.replace_bios) {
|
||||
*cycle.value = super_game_module_.ram[address];
|
||||
} else {
|
||||
*cycle.value = bios_[address];
|
||||
}
|
||||
} else if(super_game_module_.replace_ram && address < 0x8000) {
|
||||
*cycle.value = super_game_module_.ram[address];
|
||||
} else if(address >= 0x6000 && address < 0x8000) {
|
||||
*cycle.value = ram_[address & 1023];
|
||||
} else if(address >= 0x8000 && address <= cartridge_address_limit_) {
|
||||
if(is_megacart_ && address >= 0xffc0) {
|
||||
page_megacart(address);
|
||||
}
|
||||
*cycle.value = cartridge_pages_[(address >> 14)&1][address&0x3fff];
|
||||
} else {
|
||||
*cycle.value = bios_[address];
|
||||
*cycle.value = 0xff;
|
||||
}
|
||||
} else if(super_game_module_.replace_ram && address < 0x8000) {
|
||||
*cycle.value = super_game_module_.ram[address];
|
||||
} else if(address >= 0x6000 && address < 0x8000) {
|
||||
*cycle.value = ram_[address & 1023];
|
||||
} else if(address >= 0x8000 && address <= cartridge_address_limit_) {
|
||||
if(is_megacart_ && address >= 0xffc0) {
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Write:
|
||||
if(super_game_module_.replace_bios && address < 0x2000) {
|
||||
super_game_module_.ram[address] = *cycle.value;
|
||||
} else if(super_game_module_.replace_ram && address >= 0x2000 && address < 0x8000) {
|
||||
super_game_module_.ram[address] = *cycle.value;
|
||||
} else if(address >= 0x6000 && address < 0x8000) {
|
||||
ram_[address & 1023] = *cycle.value;
|
||||
} else if(is_megacart_ && address >= 0xffc0) {
|
||||
page_megacart(address);
|
||||
}
|
||||
*cycle.value = cartridge_pages_[(address >> 14)&1][address&0x3fff];
|
||||
} else {
|
||||
*cycle.value = 0xff;
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Write:
|
||||
if(super_game_module_.replace_bios && address < 0x2000) {
|
||||
super_game_module_.ram[address] = *cycle.value;
|
||||
} else if(super_game_module_.replace_ram && address >= 0x2000 && address < 0x8000) {
|
||||
super_game_module_.ram[address] = *cycle.value;
|
||||
} else if(address >= 0x6000 && address < 0x8000) {
|
||||
ram_[address & 1023] = *cycle.value;
|
||||
} else if(is_megacart_ && address >= 0xffc0) {
|
||||
page_megacart(address);
|
||||
}
|
||||
break;
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
switch((address >> 5) & 7) {
|
||||
case 5:
|
||||
*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 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();
|
||||
break;
|
||||
case 7: {
|
||||
const std::size_t joystick_id = (address&2) >> 1;
|
||||
Joystick *joystick = static_cast<Joystick *>(joysticks_[joystick_id].get());
|
||||
if(joysticks_in_keypad_mode_) {
|
||||
*cycle.value = joystick->get_keypad_input();
|
||||
} else {
|
||||
*cycle.value = joystick->get_direction_input();
|
||||
}
|
||||
|
||||
case 7: {
|
||||
const std::size_t joystick_id = (address&2) >> 1;
|
||||
Joystick *joystick = static_cast<Joystick *>(joysticks_[joystick_id].get());
|
||||
if(joysticks_in_keypad_mode_) {
|
||||
*cycle.value = joystick->get_keypad_input();
|
||||
} else {
|
||||
*cycle.value = joystick->get_direction_input();
|
||||
}
|
||||
// Hitting exactly the recommended joypad input port is an indicator that
|
||||
// this really is a ColecoVision game. The BIOS won't do this when just waiting
|
||||
// to start a game (unlike accessing the VDP and SN).
|
||||
if((address&0xfc) == 0xfc) confidence_counter_.add_hit();
|
||||
} break;
|
||||
|
||||
// Hitting exactly the recommended joypad input port is an indicator that
|
||||
// this really is a ColecoVision game. The BIOS won't do this when just waiting
|
||||
// to start a game (unlike accessing the VDP and SN).
|
||||
if((address&0xfc) == 0xfc) confidence_counter_.add_hit();
|
||||
} break;
|
||||
default:
|
||||
switch(address&0xff) {
|
||||
default: *cycle.value = 0xff; break;
|
||||
case 0x52:
|
||||
// Read AY data.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
*cycle.value = ay_.get_data_output();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
switch(address&0xff) {
|
||||
default: *cycle.value = 0xff; break;
|
||||
case 0x52:
|
||||
// Read AY data.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
*cycle.value = ay_.get_data_output();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case CPU::Z80::PartialMachineCycle::Output: {
|
||||
const int eighth = (address >> 5) & 7;
|
||||
switch(eighth) {
|
||||
case 4: case 6:
|
||||
joysticks_in_keypad_mode_ = eighth == 4;
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Output: {
|
||||
const int eighth = (address >> 5) & 7;
|
||||
switch(eighth) {
|
||||
case 4: case 6:
|
||||
joysticks_in_keypad_mode_ = eighth == 4;
|
||||
break;
|
||||
case 5:
|
||||
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 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();
|
||||
break;
|
||||
case 7:
|
||||
update_audio();
|
||||
sn76489_.set_register(*cycle.value);
|
||||
break;
|
||||
|
||||
case 7:
|
||||
update_audio();
|
||||
sn76489_.set_register(*cycle.value);
|
||||
break;
|
||||
default:
|
||||
// Catch Super Game Module accesses; it decodes more thoroughly.
|
||||
switch(address&0xff) {
|
||||
default: break;
|
||||
case 0x7f:
|
||||
super_game_module_.replace_bios = !((*cycle.value)&0x2);
|
||||
break;
|
||||
case 0x50:
|
||||
// Set AY address.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::BC1);
|
||||
ay_.set_data_input(*cycle.value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
break;
|
||||
case 0x51:
|
||||
// Set AY data.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR));
|
||||
ay_.set_data_input(*cycle.value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
break;
|
||||
case 0x53:
|
||||
super_game_module_.replace_ram = !!((*cycle.value)&0x1);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} break;
|
||||
|
||||
default:
|
||||
// Catch Super Game Module accesses; it decodes more thoroughly.
|
||||
switch(address&0xff) {
|
||||
default: break;
|
||||
case 0x7f:
|
||||
super_game_module_.replace_bios = !((*cycle.value)&0x2);
|
||||
break;
|
||||
case 0x50:
|
||||
// Set AY address.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::BC1);
|
||||
ay_.set_data_input(*cycle.value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
break;
|
||||
case 0x51:
|
||||
// Set AY data.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR));
|
||||
ay_.set_data_input(*cycle.value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
break;
|
||||
case 0x53:
|
||||
super_game_module_.replace_ram = !!((*cycle.value)&0x1);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} break;
|
||||
|
||||
default: break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
if(time_until_interrupt_ > 0) {
|
||||
@@ -335,7 +353,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
void flush() {
|
||||
update_video();
|
||||
vdp_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
@@ -345,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();
|
||||
@@ -353,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_;
|
||||
std::unique_ptr<TI::TMS9918> vdp_;
|
||||
JustInTimeActor<TI::TMS::TMS9918, HalfCycles> vdp_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
@@ -381,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;
|
||||
}
|
||||
@@ -170,7 +177,6 @@ void SerialPortVIA::set_port_output(MOS::MOS6522::Port port, uint8_t value, uint
|
||||
attention_acknowledge_level_ = !(value&0x10);
|
||||
data_level_output_ = (value&0x02);
|
||||
|
||||
// printf("[C1540] %s output is %s\n", StringForLine(::Commodore::Serial::Line::Clock), value ? "high" : "low");
|
||||
serialPort->set_output(::Commodore::Serial::Line::Clock, static_cast<::Commodore::Serial::LineLevel>(!(value&0x08)));
|
||||
update_data_line();
|
||||
}
|
||||
@@ -178,8 +184,6 @@ void SerialPortVIA::set_port_output(MOS::MOS6522::Port port, uint8_t value, uint
|
||||
}
|
||||
|
||||
void SerialPortVIA::set_serial_line_state(::Commodore::Serial::Line line, bool value) {
|
||||
// printf("[C1540] %s input is %s\n", StringForLine(line), value ? "high" : "low");
|
||||
|
||||
switch(line) {
|
||||
default: break;
|
||||
case ::Commodore::Serial::Line::Data: port_b_ = (port_b_ & ~0x01) | (value ? 0x00 : 0x01); break;
|
||||
@@ -200,7 +204,6 @@ void SerialPortVIA::set_serial_port(const std::shared_ptr<::Commodore::Serial::P
|
||||
void SerialPortVIA::update_data_line() {
|
||||
std::shared_ptr<::Commodore::Serial::Port> serialPort = serial_port_.lock();
|
||||
if(serialPort) {
|
||||
// printf("[C1540] %s output is %s\n", StringForLine(::Commodore::Serial::Line::Data), (!data_level_output_ && (attention_level_input_ != attention_acknowledge_level_)) ? "high" : "low");
|
||||
// "ATN (Attention) is an input on pin 3 of P2 and P3 that is sensed at PB7 and CA1 of UC3 after being inverted by UA1"
|
||||
serialPort->set_output(::Commodore::Serial::Line::Data,
|
||||
static_cast<::Commodore::Serial::LineLevel>(!data_level_output_ && (attention_level_input_ != attention_acknowledge_level_)));
|
||||
@@ -278,7 +281,7 @@ void DriveVIA::set_activity_observer(Activity::Observer *observer) {
|
||||
|
||||
void SerialPort::set_input(::Commodore::Serial::Line line, ::Commodore::Serial::LineLevel level) {
|
||||
std::shared_ptr<SerialPortVIA> serialPortVIA = serial_port_VIA_.lock();
|
||||
if(serialPortVIA) serialPortVIA->set_serial_line_state(line, (bool)level);
|
||||
if(serialPortVIA) serialPortVIA->set_serial_line_state(line, static_cast<bool>(level));
|
||||
}
|
||||
|
||||
void SerialPort::set_serial_port_via(const std::shared_ptr<SerialPortVIA> &serialPortVIA) {
|
||||
|
||||
@@ -143,7 +143,7 @@ class MachineBase:
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
|
||||
protected:
|
||||
CPU::MOS6502::Processor<MachineBase, false> m6502_;
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, MachineBase, false> m6502_;
|
||||
std::shared_ptr<Storage::Disk::Drive> drive_;
|
||||
|
||||
uint8_t ram_[0x800];
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "SerialBus.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
|
||||
using namespace Commodore::Serial;
|
||||
|
||||
@@ -45,12 +46,10 @@ void Bus::set_line_output_did_change(Line line) {
|
||||
for(std::weak_ptr<Port> port : ports_) {
|
||||
std::shared_ptr<Port> locked_port = port.lock();
|
||||
if(locked_port) {
|
||||
new_line_level = (LineLevel)((bool)new_line_level & (bool)locked_port->get_output(line));
|
||||
new_line_level = (LineLevel)(static_cast<bool>(new_line_level) & static_cast<bool>(locked_port->get_output(line)));
|
||||
}
|
||||
}
|
||||
|
||||
// printf("[Bus] %s is %s\n", StringForLine(line), new_line_level ? "high" : "low");
|
||||
|
||||
// post an update only if one occurred
|
||||
if(new_line_level != line_levels_[line]) {
|
||||
line_levels_[line] = new_line_level;
|
||||
@@ -69,7 +68,7 @@ void Bus::set_line_output_did_change(Line line) {
|
||||
void DebugPort::set_input(Line line, LineLevel value) {
|
||||
input_levels_[line] = value;
|
||||
|
||||
printf("[Bus] %s is %s\n", StringForLine(line), value ? "high" : "low");
|
||||
std::cout << "[Bus] " << StringForLine(line) << " is " << (value ? "high" : "low");
|
||||
if(!incoming_count_) {
|
||||
incoming_count_ = (!input_levels_[Line::Clock] && !input_levels_[Line::Data]) ? 8 : 0;
|
||||
} else {
|
||||
@@ -77,6 +76,6 @@ void DebugPort::set_input(Line line, LineLevel value) {
|
||||
incoming_byte_ = (incoming_byte_ >> 1) | (input_levels_[Line::Data] ? 0x80 : 0x00);
|
||||
}
|
||||
incoming_count_--;
|
||||
if(incoming_count_ == 0) printf("[Bus] Observed %02x\n", incoming_byte_);
|
||||
if(incoming_count_ == 0) std::cout << "[Bus] Observed value " << std::hex << int(incoming_byte_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,13 +68,13 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
BIND(F7, KeyF7);
|
||||
}
|
||||
#undef BIND
|
||||
return KeyboardMachine::Machine::KeyNotMapped;
|
||||
return KeyboardMachine::MappedMachine::KeyNotMapped;
|
||||
}
|
||||
|
||||
uint16_t *CharacterMapper::sequence_for_character(char character) {
|
||||
#define KEYS(...) {__VA_ARGS__, KeyboardMachine::Machine::KeyEndSequence}
|
||||
#define SHIFT(...) {KeyLShift, __VA_ARGS__, KeyboardMachine::Machine::KeyEndSequence}
|
||||
#define X {KeyboardMachine::Machine::KeyNotMapped}
|
||||
#define KEYS(...) {__VA_ARGS__, KeyboardMachine::MappedMachine::KeyEndSequence}
|
||||
#define SHIFT(...) {KeyLShift, __VA_ARGS__, KeyboardMachine::MappedMachine::KeyEndSequence}
|
||||
#define X {KeyboardMachine::MappedMachine::KeyNotMapped}
|
||||
static KeySequence key_sequences[] = {
|
||||
/* NUL */ X, /* SOH */ X,
|
||||
/* STX */ X, /* ETX */ X,
|
||||
|
||||
@@ -34,11 +34,11 @@ enum Key: uint16_t {
|
||||
Key1 = key(0, 0x01), Key3 = key(0, 0x02), Key5 = key(0, 0x04), Key7 = key(0, 0x08),
|
||||
Key9 = key(0, 0x10), KeyPlus = key(0, 0x20), KeyGBP = key(0, 0x40), KeyDelete = key(0, 0x80),
|
||||
|
||||
KeyRestore = 0xfffd
|
||||
KeyRestore = 0xfffd
|
||||
#undef key
|
||||
};
|
||||
|
||||
struct KeyboardMapper: public KeyboardMachine::Machine::KeyboardMapper {
|
||||
struct KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "../../../Components/6522/6522.hpp"
|
||||
|
||||
#include "../../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include "../../../Storage/Tape/Parsers/Commodore.hpp"
|
||||
|
||||
@@ -50,7 +51,7 @@ enum ROMSlot {
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
return Configurable::standard_options(
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplaySVideo | Configurable::DisplayComposite | Configurable::QuickLoadTape)
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplaySVideo | Configurable::DisplayCompositeColour | Configurable::QuickLoadTape)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -214,7 +215,7 @@ class SerialPort : public ::Commodore::Serial::Port {
|
||||
/// Receives an input change from the base serial port class, and communicates it to the user-port VIA.
|
||||
void set_input(::Commodore::Serial::Line line, ::Commodore::Serial::LineLevel level) {
|
||||
std::shared_ptr<UserPortVIA> userPortVIA = user_port_via_.lock();
|
||||
if(userPortVIA) userPortVIA->set_serial_line_state(line, (bool)level);
|
||||
if(userPortVIA) userPortVIA->set_serial_line_state(line, static_cast<bool>(level));
|
||||
}
|
||||
|
||||
/// Sets the user-port VIA with which this serial port communicates.
|
||||
@@ -281,7 +282,7 @@ class Joystick: public Inputs::ConcreteJoystick {
|
||||
class ConcreteMachine:
|
||||
public CRTMachine::Machine,
|
||||
public MediaTarget::Machine,
|
||||
public KeyboardMachine::Machine,
|
||||
public KeyboardMachine::MappedMachine,
|
||||
public JoystickMachine::Machine,
|
||||
public Configurable::Device,
|
||||
public CPU::MOS6502::BusHandler,
|
||||
@@ -294,6 +295,7 @@ class ConcreteMachine:
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::Commodore::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
m6502_(*this),
|
||||
mos6560_(mos6560_bus_handler_),
|
||||
user_port_via_port_handler_(new UserPortVIA),
|
||||
keyboard_via_port_handler_(new KeyboardVIA),
|
||||
serial_port_(new SerialPort),
|
||||
@@ -321,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) {
|
||||
@@ -377,13 +382,16 @@ class ConcreteMachine:
|
||||
if(target.region == Analyser::Static::Commodore::Target::Region::American || target.region == Analyser::Static::Commodore::Target::Region::Japanese) {
|
||||
// NTSC
|
||||
set_clock_rate(1022727);
|
||||
output_mode_ = MOS::MOS6560::OutputMode::NTSC;
|
||||
mos6560_.set_output_mode(MOS::MOS6560::OutputMode::NTSC);
|
||||
} else {
|
||||
// PAL
|
||||
set_clock_rate(1108404);
|
||||
output_mode_ = MOS::MOS6560::OutputMode::PAL;
|
||||
mos6560_.set_output_mode(MOS::MOS6560::OutputMode::PAL);
|
||||
}
|
||||
|
||||
mos6560_.set_high_frequency_cutoff(1600); // There is a 1.6Khz low-pass filter in the Vic-20.
|
||||
mos6560_.set_clock_rate(get_clock_rate());
|
||||
|
||||
// Initialise the memory maps as all pointing to nothing
|
||||
memset(processor_read_memory_map_, 0, sizeof(processor_read_memory_map_));
|
||||
memset(processor_write_memory_map_, 0, sizeof(processor_write_memory_map_));
|
||||
@@ -501,7 +509,7 @@ class ConcreteMachine:
|
||||
if((address&0xfc00) == 0x9000) {
|
||||
if(!(address&0x100)) {
|
||||
update_video();
|
||||
result &= mos6560_->get_register(address);
|
||||
result &= mos6560_.get_register(address);
|
||||
}
|
||||
if(address & 0x10) result &= user_port_via_.get_register(address);
|
||||
if(address & 0x20) result &= keyboard_via_.get_register(address);
|
||||
@@ -522,12 +530,12 @@ class ConcreteMachine:
|
||||
const uint16_t tape_buffer_pointer = static_cast<uint16_t>(ram_[0xb2]) | static_cast<uint16_t>(ram_[0xb3] << 8);
|
||||
header->serialise(&ram_[tape_buffer_pointer], 0x8000 - tape_buffer_pointer);
|
||||
hold_tape_ = true;
|
||||
printf("Found header\n");
|
||||
LOG("Vic-20: Found header");
|
||||
} else {
|
||||
// no header found, so pretend this hack never interceded
|
||||
tape_->get_tape()->set_offset(tape_position);
|
||||
hold_tape_ = false;
|
||||
printf("Didn't find header\n");
|
||||
LOG("Vic-20: Didn't find header");
|
||||
}
|
||||
|
||||
// clear status and the verify flag
|
||||
@@ -568,11 +576,11 @@ class ConcreteMachine:
|
||||
m6502_.set_value_of_register(CPU::MOS6502::Register::ProgramCounter, 0xfccf);
|
||||
*value = 0xea; // i.e. NOP implied
|
||||
hold_tape_ = true;
|
||||
printf("Found data\n");
|
||||
LOG("Vic-20: Found data");
|
||||
} else {
|
||||
tape_->get_tape()->set_offset(tape_position);
|
||||
hold_tape_ = false;
|
||||
printf("Didn't find data\n");
|
||||
LOG("Vic-20: Didn't find data");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -588,7 +596,7 @@ class ConcreteMachine:
|
||||
// The VIC is selected by bit 8 = 0
|
||||
if(!(address&0x100)) {
|
||||
update_video();
|
||||
mos6560_->set_register(address, *value);
|
||||
mos6560_.set_register(address, *value);
|
||||
}
|
||||
// The first VIA is selected by bit 4 = 1.
|
||||
if(address & 0x10) user_port_via_.set_register(address, *value);
|
||||
@@ -613,30 +621,23 @@ class ConcreteMachine:
|
||||
|
||||
void flush() {
|
||||
update_video();
|
||||
mos6560_->flush();
|
||||
mos6560_.flush();
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override final {
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void setup_output(float aspect_ratio) override final {
|
||||
mos6560_.reset(new MOS::MOS6560::MOS6560<Vic6560BusHandler>(mos6560_bus_handler_));
|
||||
mos6560_->set_high_frequency_cutoff(1600); // There is a 1.6Khz low-pass filter in the Vic-20.
|
||||
mos6560_->set_output_mode(output_mode_);
|
||||
mos6560_->set_clock_rate(get_clock_rate());
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
|
||||
mos6560_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void close_output() override final {
|
||||
mos6560_ = nullptr;
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *get_crt() override final {
|
||||
return mos6560_->get_crt();
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override final {
|
||||
mos6560_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override final {
|
||||
return mos6560_->get_speaker();
|
||||
return mos6560_.get_speaker();
|
||||
}
|
||||
|
||||
void mos6522_did_change_interrupt_status(void *mos6522) override final {
|
||||
@@ -678,7 +679,7 @@ class ConcreteMachine:
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, false);
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::Composite);
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
@@ -701,9 +702,9 @@ 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<ConcreteMachine, false> m6502_;
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
|
||||
|
||||
std::vector<uint8_t> character_rom_;
|
||||
std::vector<uint8_t> basic_rom_;
|
||||
@@ -731,8 +732,7 @@ class ConcreteMachine:
|
||||
|
||||
Cycles cycles_since_mos6560_update_;
|
||||
Vic6560BusHandler mos6560_bus_handler_;
|
||||
MOS::MOS6560::OutputMode output_mode_;
|
||||
std::unique_ptr<MOS::MOS6560::MOS6560<Vic6560BusHandler>> mos6560_;
|
||||
MOS::MOS6560::MOS6560<Vic6560BusHandler> mos6560_;
|
||||
std::shared_ptr<UserPortVIA> user_port_via_port_handler_;
|
||||
std::shared_ptr<KeyboardVIA> keyboard_via_port_handler_;
|
||||
std::shared_ptr<SerialPort> serial_port_;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/*!
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Electron {
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
return Configurable::standard_options(
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayComposite | Configurable::QuickLoadTape)
|
||||
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayCompositeColour | Configurable::QuickLoadTape)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class ConcreteMachine:
|
||||
public Machine,
|
||||
public CRTMachine::Machine,
|
||||
public MediaTarget::Machine,
|
||||
public KeyboardMachine::Machine,
|
||||
public KeyboardMachine::MappedMachine,
|
||||
public Configurable::Device,
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public Tape::Delegate,
|
||||
@@ -51,6 +51,7 @@ class ConcreteMachine:
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::Acorn::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
m6502_(*this),
|
||||
video_output_(ram_),
|
||||
sound_generator_(audio_queue_),
|
||||
speaker_(sound_generator_) {
|
||||
memset(key_states_, 0, sizeof(key_states_));
|
||||
@@ -61,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) {
|
||||
@@ -160,7 +166,7 @@ class ConcreteMachine:
|
||||
|
||||
// for the entire frame, RAM is accessible only on odd cycles; in modes below 4
|
||||
// it's also accessible only outside of the pixel regions
|
||||
cycles += video_output_->get_cycles_until_next_ram_availability(cycles_since_display_update_.as_int() + 1);
|
||||
cycles += video_output_.get_cycles_until_next_ram_availability(cycles_since_display_update_.as_int() + 1);
|
||||
} else {
|
||||
switch(address & 0xff0f) {
|
||||
case 0xfe00:
|
||||
@@ -198,8 +204,8 @@ class ConcreteMachine:
|
||||
case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f:
|
||||
if(!isReadOperation(operation)) {
|
||||
update_display();
|
||||
video_output_->set_register(address, *value);
|
||||
video_access_range_ = video_output_->get_memory_access_range();
|
||||
video_output_.set_register(address, *value);
|
||||
video_access_range_ = video_output_.get_memory_access_range();
|
||||
queue_next_display_interrupt();
|
||||
}
|
||||
break;
|
||||
@@ -373,16 +379,12 @@ class ConcreteMachine:
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void setup_output(float aspect_ratio) override final {
|
||||
video_output_.reset(new VideoOutput(ram_));
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
|
||||
video_output_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
void close_output() override final {
|
||||
video_output_.reset();
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *get_crt() override final {
|
||||
return video_output_->get_crt();
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
video_output_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override final {
|
||||
@@ -436,7 +438,7 @@ class ConcreteMachine:
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, false);
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::Composite);
|
||||
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
@@ -508,12 +510,12 @@ 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>());
|
||||
}
|
||||
}
|
||||
|
||||
inline void queue_next_display_interrupt() {
|
||||
VideoOutput::Interrupt next_interrupt = video_output_->get_next_interrupt();
|
||||
VideoOutput::Interrupt next_interrupt = video_output_.get_next_interrupt();
|
||||
cycles_until_display_interrupt_ = next_interrupt.cycles;
|
||||
next_display_interrupt_ = next_interrupt.interrupt;
|
||||
}
|
||||
@@ -541,7 +543,7 @@ class ConcreteMachine:
|
||||
m6502_.set_irq_line(interrupt_status_ & 1);
|
||||
}
|
||||
|
||||
CPU::MOS6502::Processor<ConcreteMachine, false> m6502_;
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
|
||||
|
||||
// Things that directly constitute the memory map.
|
||||
uint8_t roms_[16][16384];
|
||||
@@ -583,7 +585,7 @@ class ConcreteMachine:
|
||||
int shift_restart_counter_ = 0;
|
||||
|
||||
// Outputs
|
||||
std::unique_ptr<VideoOutput> video_output_;
|
||||
VideoOutput video_output_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
SoundGenerator sound_generator_;
|
||||
|
||||
@@ -31,7 +31,6 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
BIND(Quote, KeyColon);
|
||||
|
||||
BIND(Escape, KeyEscape);
|
||||
BIND(Equals, KeyBreak);
|
||||
BIND(F12, KeyBreak);
|
||||
|
||||
BIND(Left, KeyLeft); BIND(Right, KeyRight); BIND(Up, KeyUp); BIND(Down, KeyDown);
|
||||
@@ -56,10 +55,10 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
|
||||
}
|
||||
|
||||
uint16_t *CharacterMapper::sequence_for_character(char character) {
|
||||
#define KEYS(...) {__VA_ARGS__, KeyboardMachine::Machine::KeyEndSequence}
|
||||
#define SHIFT(...) {KeyShift, __VA_ARGS__, KeyboardMachine::Machine::KeyEndSequence}
|
||||
#define CTRL(...) {KeyControl, __VA_ARGS__, KeyboardMachine::Machine::KeyEndSequence}
|
||||
#define X {KeyboardMachine::Machine::KeyNotMapped}
|
||||
#define KEYS(...) {__VA_ARGS__, KeyboardMachine::MappedMachine::KeyEndSequence}
|
||||
#define SHIFT(...) {KeyShift, __VA_ARGS__, KeyboardMachine::MappedMachine::KeyEndSequence}
|
||||
#define CTRL(...) {KeyControl, __VA_ARGS__, KeyboardMachine::MappedMachine::KeyEndSequence}
|
||||
#define X {KeyboardMachine::MappedMachine::KeyNotMapped}
|
||||
static KeySequence key_sequences[] = {
|
||||
/* NUL */ X, /* SOH */ X,
|
||||
/* STX */ X, /* ETX */ X,
|
||||
|
||||
@@ -33,8 +33,8 @@ enum Key: uint16_t {
|
||||
KeyBreak = 0xfffd,
|
||||
};
|
||||
|
||||
struct KeyboardMapper: public KeyboardMachine::Machine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key);
|
||||
struct KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) override;
|
||||
};
|
||||
|
||||
struct CharacterMapper: public ::Utility::CharacterMapper {
|
||||
|
||||
@@ -34,41 +34,30 @@ namespace {
|
||||
static const int real_time_clock_interrupt_2 = 56704;
|
||||
static const int display_end_interrupt_1 = (first_graphics_line + display_end_interrupt_line)*cycles_per_line;
|
||||
static const int display_end_interrupt_2 = (first_graphics_line + field_divider_line + display_end_interrupt_line)*cycles_per_line;
|
||||
|
||||
struct FourBPPBookender: public Outputs::CRT::TextureBuilder::Bookender {
|
||||
void add_bookends(uint8_t *const left_value, uint8_t *const right_value, uint8_t *left_bookend, uint8_t *right_bookend) {
|
||||
*left_bookend = static_cast<uint8_t>(((*left_value) & 0x0f) | (((*left_value) & 0x0f) << 4));
|
||||
*right_bookend = static_cast<uint8_t>(((*right_value) & 0xf0) | (((*right_value) & 0xf0) >> 4));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
VideoOutput::VideoOutput(uint8_t *memory) : ram_(memory) {
|
||||
VideoOutput::VideoOutput(uint8_t *memory) :
|
||||
ram_(memory),
|
||||
crt_(crt_cycles_per_line,
|
||||
1,
|
||||
Outputs::Display::Type::PAL50,
|
||||
Outputs::Display::InputDataType::Red1Green1Blue1) {
|
||||
memset(palette_, 0xf, sizeof(palette_));
|
||||
setup_screen_map();
|
||||
setup_base_address();
|
||||
|
||||
crt_.reset(new Outputs::CRT::CRT(crt_cycles_per_line, 8, Outputs::CRT::DisplayType::PAL50, 1));
|
||||
crt_->set_rgb_sampling_function(
|
||||
"vec3 rgb_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate)"
|
||||
"{"
|
||||
"uint texValue = texture(sampler, coordinate).r;"
|
||||
"texValue >>= 4 - (int(icoordinate.x) & 4);"
|
||||
"return vec3( uvec3(texValue) & uvec3(4u, 2u, 1u));"
|
||||
"}");
|
||||
crt_->set_integer_coordinate_multiplier(8.0f);
|
||||
std::unique_ptr<Outputs::CRT::TextureBuilder::Bookender> bookender(new FourBPPBookender);
|
||||
crt_->set_bookender(std::move(bookender));
|
||||
// TODO: as implied below, I've introduced a clock's latency into the graphics pipeline somehow. Investigate.
|
||||
crt_->set_visible_area(crt_->get_rect_for_area(first_graphics_line - 1, 256, (first_graphics_cycle+1) * crt_cycles_multiplier, 80 * crt_cycles_multiplier, 4.0f / 3.0f));
|
||||
crt_.set_visible_area(crt_.get_rect_for_area(first_graphics_line - 1, 256, (first_graphics_cycle+1) * crt_cycles_multiplier, 80 * crt_cycles_multiplier, 4.0f / 3.0f));
|
||||
}
|
||||
|
||||
// MARK: - CRT getter
|
||||
void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::CRT::CRT *VideoOutput::get_crt() {
|
||||
return crt_.get();
|
||||
void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
// MARK: - Display update methods
|
||||
@@ -98,33 +87,33 @@ void VideoOutput::start_pixel_line() {
|
||||
}
|
||||
|
||||
void VideoOutput::end_pixel_line() {
|
||||
if(current_output_target_) {
|
||||
const unsigned int data_length = static_cast<unsigned int>(current_output_target_ - initial_output_target_);
|
||||
crt_->output_data(data_length * current_output_divider_, data_length);
|
||||
const int data_length = int(current_output_target_ - initial_output_target_);
|
||||
if(data_length) {
|
||||
crt_.output_data(data_length * current_output_divider_, size_t(data_length));
|
||||
}
|
||||
current_character_row_++;
|
||||
}
|
||||
|
||||
void VideoOutput::output_pixels(unsigned int number_of_cycles) {
|
||||
void VideoOutput::output_pixels(int number_of_cycles) {
|
||||
if(!number_of_cycles) return;
|
||||
|
||||
if(is_blank_line_) {
|
||||
crt_->output_blank(number_of_cycles * crt_cycles_multiplier);
|
||||
crt_.output_blank(number_of_cycles * crt_cycles_multiplier);
|
||||
} else {
|
||||
unsigned int divider = 1;
|
||||
int divider = 1;
|
||||
switch(screen_mode_) {
|
||||
case 0: case 3: divider = 2; break;
|
||||
case 1: case 4: case 6: divider = 4; break;
|
||||
case 2: case 5: divider = 8; break;
|
||||
case 0: case 3: divider = 1; break;
|
||||
case 1: case 4: case 6: divider = 2; break;
|
||||
case 2: case 5: divider = 4; break;
|
||||
}
|
||||
|
||||
if(!initial_output_target_ || divider != current_output_divider_) {
|
||||
if(current_output_target_) {
|
||||
const unsigned int data_length = static_cast<unsigned int>(current_output_target_ - initial_output_target_);
|
||||
crt_->output_data(data_length * current_output_divider_, data_length);
|
||||
const int data_length = int(current_output_target_ - initial_output_target_);
|
||||
if(data_length) {
|
||||
crt_.output_data(data_length * current_output_divider_, size_t(data_length));
|
||||
}
|
||||
current_output_divider_ = divider;
|
||||
initial_output_target_ = current_output_target_ = crt_->allocate_write_area(640 / current_output_divider_, 4);
|
||||
initial_output_target_ = current_output_target_ = crt_.begin_data(size_t(640 / current_output_divider_), size_t(8 / divider));
|
||||
}
|
||||
|
||||
#define get_pixel() \
|
||||
@@ -139,95 +128,95 @@ void VideoOutput::output_pixels(unsigned int number_of_cycles) {
|
||||
if(initial_output_target_) {
|
||||
while(number_of_cycles--) {
|
||||
get_pixel();
|
||||
*reinterpret_cast<uint32_t *>(current_output_target_) = palette_tables_.eighty1bpp[last_pixel_byte_];
|
||||
current_output_target_ += 4;
|
||||
*reinterpret_cast<uint64_t *>(current_output_target_) = palette_tables_.eighty1bpp[last_pixel_byte_];
|
||||
current_output_target_ += 8;
|
||||
current_pixel_column_++;
|
||||
}
|
||||
} else current_output_target_ += 4*number_of_cycles;
|
||||
} else current_output_target_ += 8*number_of_cycles;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if(initial_output_target_) {
|
||||
while(number_of_cycles--) {
|
||||
get_pixel();
|
||||
*reinterpret_cast<uint16_t *>(current_output_target_) = palette_tables_.eighty2bpp[last_pixel_byte_];
|
||||
current_output_target_ += 2;
|
||||
*reinterpret_cast<uint32_t *>(current_output_target_) = palette_tables_.eighty2bpp[last_pixel_byte_];
|
||||
current_output_target_ += 4;
|
||||
current_pixel_column_++;
|
||||
}
|
||||
} else current_output_target_ += 2*number_of_cycles;
|
||||
} else current_output_target_ += 4*number_of_cycles;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
if(initial_output_target_) {
|
||||
while(number_of_cycles--) {
|
||||
get_pixel();
|
||||
*current_output_target_ = palette_tables_.eighty4bpp[last_pixel_byte_];
|
||||
current_output_target_ += 1;
|
||||
*reinterpret_cast<uint16_t *>(current_output_target_) = palette_tables_.eighty4bpp[last_pixel_byte_];
|
||||
current_output_target_ += 2;
|
||||
current_pixel_column_++;
|
||||
}
|
||||
} else current_output_target_ += number_of_cycles;
|
||||
} else current_output_target_ += 2*number_of_cycles;
|
||||
break;
|
||||
|
||||
case 4: case 6:
|
||||
if(initial_output_target_) {
|
||||
if(current_pixel_column_&1) {
|
||||
last_pixel_byte_ <<= 4;
|
||||
*reinterpret_cast<uint16_t *>(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_];
|
||||
current_output_target_ += 2;
|
||||
*reinterpret_cast<uint32_t *>(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_];
|
||||
current_output_target_ += 4;
|
||||
|
||||
number_of_cycles--;
|
||||
current_pixel_column_++;
|
||||
}
|
||||
while(number_of_cycles > 1) {
|
||||
get_pixel();
|
||||
*reinterpret_cast<uint16_t *>(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_];
|
||||
current_output_target_ += 2;
|
||||
*reinterpret_cast<uint32_t *>(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_];
|
||||
current_output_target_ += 4;
|
||||
|
||||
last_pixel_byte_ <<= 4;
|
||||
*reinterpret_cast<uint16_t *>(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_];
|
||||
current_output_target_ += 2;
|
||||
*reinterpret_cast<uint32_t *>(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_];
|
||||
current_output_target_ += 4;
|
||||
|
||||
number_of_cycles -= 2;
|
||||
current_pixel_column_+=2;
|
||||
}
|
||||
if(number_of_cycles) {
|
||||
get_pixel();
|
||||
*reinterpret_cast<uint16_t *>(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_];
|
||||
current_output_target_ += 2;
|
||||
*reinterpret_cast<uint32_t *>(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_];
|
||||
current_output_target_ += 4;
|
||||
current_pixel_column_++;
|
||||
}
|
||||
} else current_output_target_ += 2 * number_of_cycles;
|
||||
} else current_output_target_ += 4 * number_of_cycles;
|
||||
break;
|
||||
|
||||
case 5:
|
||||
if(initial_output_target_) {
|
||||
if(current_pixel_column_&1) {
|
||||
last_pixel_byte_ <<= 2;
|
||||
*current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_];
|
||||
current_output_target_ += 1;
|
||||
*reinterpret_cast<uint16_t *>(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_];
|
||||
current_output_target_ += 2;
|
||||
|
||||
number_of_cycles--;
|
||||
current_pixel_column_++;
|
||||
}
|
||||
while(number_of_cycles > 1) {
|
||||
get_pixel();
|
||||
*current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_];
|
||||
current_output_target_ += 1;
|
||||
*reinterpret_cast<uint16_t *>(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_];
|
||||
current_output_target_ += 2;
|
||||
|
||||
last_pixel_byte_ <<= 2;
|
||||
*current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_];
|
||||
current_output_target_ += 1;
|
||||
*reinterpret_cast<uint16_t *>(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_];
|
||||
current_output_target_ += 2;
|
||||
|
||||
number_of_cycles -= 2;
|
||||
current_pixel_column_+=2;
|
||||
}
|
||||
if(number_of_cycles) {
|
||||
get_pixel();
|
||||
*current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_];
|
||||
current_output_target_ += 1;
|
||||
*reinterpret_cast<uint16_t *>(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_];
|
||||
current_output_target_ += 2;
|
||||
current_pixel_column_++;
|
||||
}
|
||||
} else current_output_target_ += number_of_cycles;
|
||||
} else current_output_target_ += 2*number_of_cycles;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -241,16 +230,16 @@ void VideoOutput::run_for(const Cycles cycles) {
|
||||
while(number_of_cycles) {
|
||||
int draw_action_length = screen_map_[screen_map_pointer_].length;
|
||||
int time_left_in_action = std::min(number_of_cycles, draw_action_length - cycles_into_draw_action_);
|
||||
if(screen_map_[screen_map_pointer_].type == DrawAction::Pixels) output_pixels(static_cast<unsigned int>(time_left_in_action));
|
||||
if(screen_map_[screen_map_pointer_].type == DrawAction::Pixels) output_pixels(time_left_in_action);
|
||||
|
||||
number_of_cycles -= time_left_in_action;
|
||||
cycles_into_draw_action_ += time_left_in_action;
|
||||
if(cycles_into_draw_action_ == draw_action_length) {
|
||||
switch(screen_map_[screen_map_pointer_].type) {
|
||||
case DrawAction::Sync: crt_->output_sync(static_cast<unsigned int>(draw_action_length * crt_cycles_multiplier)); break;
|
||||
case DrawAction::ColourBurst: crt_->output_default_colour_burst(static_cast<unsigned int>(draw_action_length * crt_cycles_multiplier)); break;
|
||||
case DrawAction::Blank: crt_->output_blank(static_cast<unsigned int>(draw_action_length * crt_cycles_multiplier)); break;
|
||||
case DrawAction::Pixels: end_pixel_line(); break;
|
||||
case DrawAction::Sync: crt_.output_sync(draw_action_length * crt_cycles_multiplier); break;
|
||||
case DrawAction::ColourBurst: crt_.output_default_colour_burst(draw_action_length * crt_cycles_multiplier); break;
|
||||
case DrawAction::Blank: crt_.output_blank(draw_action_length * crt_cycles_multiplier); break;
|
||||
case DrawAction::Pixels: end_pixel_line(); break;
|
||||
}
|
||||
screen_map_pointer_ = (screen_map_pointer_ + 1) % screen_map_.size();
|
||||
cycles_into_draw_action_ = 0;
|
||||
@@ -310,27 +299,37 @@ void VideoOutput::set_register(int address, uint8_t value) {
|
||||
}
|
||||
|
||||
// regenerate all palette tables for now
|
||||
#define pack(a, b) static_cast<uint8_t>((a << 4) | (b))
|
||||
for(int byte = 0; byte < 256; byte++) {
|
||||
uint8_t *target = reinterpret_cast<uint8_t *>(&palette_tables_.forty1bpp[byte]);
|
||||
target[0] = pack(palette_[(byte&0x80) >> 4], palette_[(byte&0x40) >> 3]);
|
||||
target[1] = pack(palette_[(byte&0x20) >> 2], palette_[(byte&0x10) >> 1]);
|
||||
target[0] = palette_[(byte&0x80) >> 4];
|
||||
target[1] = palette_[(byte&0x40) >> 3];
|
||||
target[2] = palette_[(byte&0x20) >> 2];
|
||||
target[3] = palette_[(byte&0x10) >> 1];
|
||||
|
||||
target = reinterpret_cast<uint8_t *>(&palette_tables_.eighty2bpp[byte]);
|
||||
target[0] = pack(palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)], palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]);
|
||||
target[1] = pack(palette_[((byte&0x20) >> 2) | ((byte&0x02) >> 0)], palette_[((byte&0x10) >> 1) | ((byte&0x01) << 1)]);
|
||||
target[0] = palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)];
|
||||
target[1] = palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)];
|
||||
target[2] = palette_[((byte&0x20) >> 2) | ((byte&0x02) >> 0)];
|
||||
target[3] = palette_[((byte&0x10) >> 1) | ((byte&0x01) << 1)];
|
||||
|
||||
target = reinterpret_cast<uint8_t *>(&palette_tables_.eighty1bpp[byte]);
|
||||
target[0] = pack(palette_[(byte&0x80) >> 4], palette_[(byte&0x40) >> 3]);
|
||||
target[1] = pack(palette_[(byte&0x20) >> 2], palette_[(byte&0x10) >> 1]);
|
||||
target[2] = pack(palette_[(byte&0x08) >> 0], palette_[(byte&0x04) << 1]);
|
||||
target[3] = pack(palette_[(byte&0x02) << 2], palette_[(byte&0x01) << 3]);
|
||||
target[0] = palette_[(byte&0x80) >> 4];
|
||||
target[1] = palette_[(byte&0x40) >> 3];
|
||||
target[2] = palette_[(byte&0x20) >> 2];
|
||||
target[3] = palette_[(byte&0x10) >> 1];
|
||||
target[4] = palette_[(byte&0x08) >> 0];
|
||||
target[5] = palette_[(byte&0x04) << 1];
|
||||
target[6] = palette_[(byte&0x02) << 2];
|
||||
target[7] = palette_[(byte&0x01) << 3];
|
||||
|
||||
palette_tables_.forty2bpp[byte] = pack( palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)], palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]);
|
||||
palette_tables_.eighty4bpp[byte] = pack( palette_[((byte&0x80) >> 4) | ((byte&0x20) >> 3) | ((byte&0x08) >> 2) | ((byte&0x02) >> 1)],
|
||||
palette_[((byte&0x40) >> 3) | ((byte&0x10) >> 2) | ((byte&0x04) >> 1) | ((byte&0x01) >> 0)]);
|
||||
target = reinterpret_cast<uint8_t *>(&palette_tables_.forty2bpp[byte]);
|
||||
target[0] = palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)];
|
||||
target[1] = palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)];
|
||||
|
||||
target = reinterpret_cast<uint8_t *>(&palette_tables_.eighty4bpp[byte]);
|
||||
target[0] = palette_[((byte&0x80) >> 4) | ((byte&0x20) >> 3) | ((byte&0x08) >> 2) | ((byte&0x02) >> 1)];
|
||||
target[1] = palette_[((byte&0x40) >> 3) | ((byte&0x10) >> 2) | ((byte&0x04) >> 1) | ((byte&0x01) >> 0)];
|
||||
}
|
||||
#undef pack
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -404,7 +403,7 @@ unsigned int VideoOutput::get_cycles_until_next_ram_availability(int from_time)
|
||||
if(current_line >= output_position_line) {
|
||||
// Get the number of lines since then if still in the same frame.
|
||||
int lines_since_output_position = current_line - output_position_line;
|
||||
|
||||
|
||||
// Therefore get the character row at the proposed time, modulo 10.
|
||||
implied_row = (current_character_row_ + lines_since_output_position) % 10;
|
||||
} else {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "Interrupts.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Electron {
|
||||
|
||||
/*!
|
||||
@@ -25,17 +27,21 @@ namespace Electron {
|
||||
class VideoOutput {
|
||||
public:
|
||||
/*!
|
||||
Instantiates a VideoOutput that will read its pixels from @c memory. The pointer supplied
|
||||
should be to address 0 in the unexpanded Electron's memory map.
|
||||
Instantiates a VideoOutput that will read its pixels from @c memory.
|
||||
|
||||
The pointer supplied should be to address 0 in the unexpanded Electron's memory map.
|
||||
*/
|
||||
VideoOutput(uint8_t *memory);
|
||||
|
||||
/// @returns the CRT to which output is being painted.
|
||||
Outputs::CRT::CRT *get_crt();
|
||||
|
||||
/// Produces the next @c cycles of video output.
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
/// Sets the destination for output.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Sets the type of output.
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
/*!
|
||||
Writes @c value to the register at @c address. May mutate the results of @c get_next_interrupt,
|
||||
@c get_cycles_until_next_ram_availability and @c get_memory_access_range.
|
||||
@@ -77,7 +83,7 @@ class VideoOutput {
|
||||
private:
|
||||
inline void start_pixel_line();
|
||||
inline void end_pixel_line();
|
||||
inline void output_pixels(unsigned int number_of_cycles);
|
||||
inline void output_pixels(int number_of_cycles);
|
||||
inline void setup_base_address();
|
||||
|
||||
int output_position_ = 0;
|
||||
@@ -90,11 +96,11 @@ class VideoOutput {
|
||||
|
||||
uint8_t *ram_;
|
||||
struct {
|
||||
uint16_t forty1bpp[256];
|
||||
uint8_t forty2bpp[256];
|
||||
uint32_t eighty1bpp[256];
|
||||
uint16_t eighty2bpp[256];
|
||||
uint8_t eighty4bpp[256];
|
||||
uint32_t forty1bpp[256];
|
||||
uint16_t forty2bpp[256];
|
||||
uint64_t eighty1bpp[256];
|
||||
uint32_t eighty2bpp[256];
|
||||
uint16_t eighty4bpp[256];
|
||||
} palette_tables_;
|
||||
|
||||
// Display generation.
|
||||
@@ -109,9 +115,8 @@ class VideoOutput {
|
||||
// CRT output
|
||||
uint8_t *current_output_target_ = nullptr;
|
||||
uint8_t *initial_output_target_ = nullptr;
|
||||
unsigned int current_output_divider_ = 1;
|
||||
|
||||
std::unique_ptr<Outputs::CRT::CRT> crt_;
|
||||
int current_output_divider_ = 1;
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
struct DrawAction {
|
||||
enum Type {
|
||||
|
||||
@@ -10,27 +10,27 @@
|
||||
|
||||
using namespace KeyboardMachine;
|
||||
|
||||
Machine::Machine() {
|
||||
MappedMachine::MappedMachine() {
|
||||
keyboard_.set_delegate(this);
|
||||
}
|
||||
|
||||
void Machine::keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) {
|
||||
void MappedMachine::keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) {
|
||||
uint16_t mapped_key = get_keyboard_mapper()->mapped_key_for_key(key);
|
||||
if(mapped_key != KeyNotMapped) set_key_state(mapped_key, is_pressed);
|
||||
}
|
||||
|
||||
void Machine::reset_all_keys(Inputs::Keyboard *keyboard) {
|
||||
void MappedMachine::reset_all_keys(Inputs::Keyboard *keyboard) {
|
||||
// TODO: unify naming.
|
||||
clear_all_keys();
|
||||
}
|
||||
|
||||
Inputs::Keyboard &Machine::get_keyboard() {
|
||||
Inputs::Keyboard &MappedMachine::get_keyboard() {
|
||||
return keyboard_;
|
||||
}
|
||||
|
||||
void Machine::type_string(const std::string &) {
|
||||
}
|
||||
|
||||
Machine::KeyboardMapper *Machine::get_keyboard_mapper() {
|
||||
MappedMachine::KeyboardMapper *MappedMachine::get_keyboard_mapper() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -33,13 +33,10 @@ struct KeyActions {
|
||||
};
|
||||
|
||||
/*!
|
||||
Describes the full functionality of being an emulated machine with a keyboard: not just being
|
||||
able to receive key actions, but being able to vend a generic keyboard and a keyboard mapper.
|
||||
Describes an emulated machine which exposes a keyboard and accepts a typed string.
|
||||
*/
|
||||
class Machine: public Inputs::Keyboard::Delegate, public KeyActions {
|
||||
class Machine: public KeyActions {
|
||||
public:
|
||||
Machine();
|
||||
|
||||
/*!
|
||||
Causes the machine to attempt to type the supplied string.
|
||||
|
||||
@@ -50,7 +47,16 @@ class Machine: public Inputs::Keyboard::Delegate, public KeyActions {
|
||||
/*!
|
||||
Provides a destination for keyboard input.
|
||||
*/
|
||||
virtual Inputs::Keyboard &get_keyboard();
|
||||
virtual Inputs::Keyboard &get_keyboard() = 0;
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides a base class for machines that want to provide a keyboard mapper,
|
||||
allowing automatic mapping from keyboard inputs to KeyActions.
|
||||
*/
|
||||
class MappedMachine: public Inputs::Keyboard::Delegate, public Machine {
|
||||
public:
|
||||
MappedMachine();
|
||||
|
||||
/*!
|
||||
A keyboard mapper attempts to provide a physical mapping between host keys and emulated keys.
|
||||
@@ -76,6 +82,12 @@ class Machine: public Inputs::Keyboard::Delegate, public KeyActions {
|
||||
*/
|
||||
virtual KeyboardMapper *get_keyboard_mapper();
|
||||
|
||||
/*!
|
||||
Provides a keyboard that obtains this machine's keyboard mapper, maps
|
||||
the key and supplies it via the KeyActions.
|
||||
*/
|
||||
virtual Inputs::Keyboard &get_keyboard() override;
|
||||
|
||||
private:
|
||||
void keyboard_did_change_key(Inputs::Keyboard *keyboard, Inputs::Keyboard::Key key, bool is_pressed) override;
|
||||
void reset_all_keys(Inputs::Keyboard *keyboard) override;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user