mirror of
https://github.com/ares-emulator/ares.git
synced 2026-11-07 17:01:59 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd73766522 | ||
|
|
d3b8a29910 | ||
|
|
05232171d4 | ||
|
|
15ef1ac2d5 | ||
|
|
234c99911f | ||
|
|
94e66350a1 | ||
|
|
c4f133ac0b | ||
|
|
4307bf2eff | ||
|
|
17f29369c4 | ||
|
|
dc4f4659d4 | ||
|
|
e9639ebae3 | ||
|
|
bdf0b044e8 | ||
|
|
26c56bb4bc | ||
|
|
9c5ed3f68d | ||
|
|
b6890c9eff | ||
|
|
035fb15c71 | ||
|
|
6fd11e291f | ||
|
|
1c07ebf70c | ||
|
|
569acf7ba1 | ||
|
|
47bab688e6 | ||
|
|
460fec09a0 | ||
|
|
5d093ad9e6 | ||
|
|
3474c5c1bc | ||
|
|
d400eee976 | ||
|
|
8128704f23 | ||
|
|
a774671dc0 | ||
|
|
3f268697d2 | ||
|
|
26382de232 | ||
|
|
9c65426aab | ||
|
|
4355718dda | ||
|
|
022805be81 | ||
|
|
c76aaffa6e | ||
|
|
9408cb43d4 | ||
|
|
4565c732bd | ||
|
|
a776c509bd | ||
|
|
2976a1ab6b | ||
|
|
4cb8d92b44 | ||
|
|
17813a3ccd | ||
|
|
9c9e4148b0 | ||
|
|
b5f2943acb | ||
|
|
a137e844ac | ||
|
|
b71dabe7a9 | ||
|
|
e40b0db1ed | ||
|
|
1e882cdd12 | ||
|
|
09d0c939c5 | ||
|
|
412c554e9c | ||
|
|
fd12c9174e | ||
|
|
b7cef2049b | ||
|
|
0f6b50412b | ||
|
|
6d6e1037e8 | ||
|
|
b15d4d378c | ||
|
|
2bd312e602 | ||
|
|
af4cbb04f0 | ||
|
|
dee49f55ae | ||
|
|
e6130f66e7 | ||
|
|
5de16c334d | ||
|
|
7b51c8ab71 | ||
|
|
66a53e2ed8 | ||
|
|
24ab7df1e4 | ||
|
|
8381b5ae05 | ||
|
|
0b87e304a0 | ||
|
|
9092f818b9 | ||
|
|
a6869c924d | ||
|
|
a144d3150a | ||
|
|
5ca93ea4d2 | ||
|
|
cc6b83228f | ||
|
|
4a71d93bb6 | ||
|
|
751f615205 | ||
|
|
39ca282454 | ||
|
|
f0c946b55e | ||
|
|
742cc93106 | ||
|
|
6495f9ea0f | ||
|
|
de92243f40 | ||
|
|
403c81a8cb | ||
|
|
b255ec7f31 | ||
|
|
22af611ad5 | ||
|
|
73d1382c61 | ||
|
|
7f40b23068 | ||
|
|
99f2d442a7 | ||
|
|
6c629246bc | ||
|
|
bd4e506a19 | ||
|
|
0f15baebfd | ||
|
|
6cf7770c4c | ||
|
|
cd3cc0087e | ||
|
|
5578e7ca89 | ||
|
|
eac791413a | ||
|
|
65e69f7159 | ||
|
|
fb7544b169 | ||
|
|
a2d94ebba1 | ||
|
|
e4217366cf | ||
|
|
430def86f2 | ||
|
|
4bfccfc3d5 | ||
|
|
e3b53a6ec9 | ||
|
|
8a22cd8096 | ||
|
|
3918992a41 | ||
|
|
f42f60d744 | ||
|
|
fc83454343 | ||
|
|
f23eb39ad5 | ||
|
|
f67757e35e | ||
|
|
190afd8ad2 | ||
|
|
e2cdee5038 | ||
|
|
85adb9862f | ||
|
|
0a78d55452 | ||
|
|
e5d619c238 | ||
|
|
a840e6427e | ||
|
|
9cb73a6842 | ||
|
|
901f57e125 | ||
|
|
8da242f5a6 | ||
|
|
fa2105ea11 | ||
|
|
30131cd32c | ||
|
|
b80f67d383 | ||
|
|
53ba720c8d | ||
|
|
274aabdf4a | ||
|
|
cdabe5c5a6 | ||
|
|
5fdc7a8c0b | ||
|
|
ec5e82ae76 | ||
|
|
1bad216467 | ||
|
|
1fcef61b84 | ||
|
|
f1382f7780 | ||
|
|
6ed19d3115 | ||
|
|
7806042b31 | ||
|
|
433d07fbdb | ||
|
|
5c6607fa9b | ||
|
|
3c63fbcfb4 | ||
|
|
45c2291261 | ||
|
|
3cac8a1863 | ||
|
|
d5c0568492 | ||
|
|
0b9b8bce6b | ||
|
|
0583153727 | ||
|
|
1cc7dfd4f6 | ||
|
|
e1b0d47440 | ||
|
|
be3c7898ce | ||
|
|
449b93716f | ||
|
|
6dc3f3306c | ||
|
|
91b112279a | ||
|
|
c634ac3639 | ||
|
|
743ef1f2cd | ||
|
|
2e7a877a96 | ||
|
|
1337a5c013 | ||
|
|
459ceb2e22 | ||
|
|
0d14e49319 | ||
|
|
9293695459 | ||
|
|
5f2f7dc0d4 | ||
|
|
7032bc6ef0 | ||
|
|
acf213fa5f | ||
|
|
4ceadb9549 | ||
|
|
2ce75dde67 | ||
|
|
31526ded91 | ||
|
|
c3c2b79015 | ||
|
|
8c41639a76 | ||
|
|
8bcb10f36b | ||
|
|
d6430da5dc | ||
|
|
57a4bec25d | ||
|
|
920dfb3de8 | ||
|
|
c02233f815 | ||
|
|
0ec4b5f953 | ||
|
|
aa5df08e2b | ||
|
|
e70cb9fb58 | ||
|
|
40474c4987 | ||
|
|
53d8b81ade | ||
|
|
bb3c36cd96 | ||
|
|
5ee7e9a5ba | ||
|
|
6fcaa213fb | ||
|
|
658aad5dd7 | ||
|
|
e67f0cefc5 | ||
|
|
90c655b944 | ||
|
|
0aafd85789 | ||
|
|
67b0bf7aac | ||
|
|
11d887c99c | ||
|
|
7c03fb208f | ||
|
|
b33edf5f90 | ||
|
|
83369fd40f | ||
|
|
6ee42f25b4 | ||
|
|
fe388a7fcc | ||
|
|
c7d667fa07 | ||
|
|
cc0e405780 | ||
|
|
c21adc7b57 | ||
|
|
232f85e39c | ||
|
|
c951652efb | ||
|
|
f605d52032 | ||
|
|
bdd585a563 | ||
|
|
54fc194182 | ||
|
|
4c39ca23eb | ||
|
|
8ea87c75e4 | ||
|
|
7f0f91b3da | ||
|
|
164f5648bf | ||
|
|
19cce7dbba | ||
|
|
765c0d4181 | ||
|
|
fbc8f22821 | ||
|
|
14f4fc0173 | ||
|
|
6920622b7e | ||
|
|
c42bb2626d | ||
|
|
d45be3297f | ||
|
|
54adf82264 | ||
|
|
7d1bbf7c04 | ||
|
|
d17ae2f36d | ||
|
|
b9f8347d57 | ||
|
|
41af52ec27 | ||
|
|
87daf87fbb | ||
|
|
2a5b9fe74e | ||
|
|
bb91ff1136 | ||
|
|
5d7496c6f1 | ||
|
|
18692c3ced | ||
|
|
bc3af76716 | ||
|
|
03dd55bbda | ||
|
|
ffab36ed99 | ||
|
|
8ad8803765 | ||
|
|
3198e0e88e | ||
|
|
999d858fa7 | ||
|
|
a752515d2d | ||
|
|
2460b01b27 | ||
|
|
2af310528b | ||
|
|
a1be16873f | ||
|
|
8b4f1fa16e | ||
|
|
882fc4df1f | ||
|
|
997077527b | ||
|
|
dd834d2c5d | ||
|
|
50b9786587 | ||
|
|
fbb6e72e02 | ||
|
|
c152ef5639 | ||
|
|
49af8ab6a2 | ||
|
|
68134837d4 | ||
|
|
bb75c2f058 | ||
|
|
105e8da924 | ||
|
|
b0de6c11d2 | ||
|
|
b89a0bcce1 | ||
|
|
04f8b9cb4d | ||
|
|
c139a4d4ad | ||
|
|
28f05b0297 | ||
|
|
39b5d798b9 | ||
|
|
7e5841d84a | ||
|
|
8c3961cd6e | ||
|
|
e6517f5f57 | ||
|
|
cd604bf619 | ||
|
|
85008a663e | ||
|
|
8031dcd3f2 | ||
|
|
f0b8a36b6a | ||
|
|
36aed53124 | ||
|
|
1d8c89a50f | ||
|
|
60007e8853 | ||
|
|
3542371b76 | ||
|
|
a8882a1d89 | ||
|
|
86fa83fc2b | ||
|
|
71ddda52b5 | ||
|
|
be0504e1c2 | ||
|
|
a34a6ab1df | ||
|
|
515cb06729 | ||
|
|
2d852d6a9b | ||
|
|
5b8abb5717 | ||
|
|
496409c199 | ||
|
|
9e6be60b9e | ||
|
|
fdbd036bd1 | ||
|
|
f241c4095d | ||
|
|
094454e52f | ||
|
|
dabeb68387 | ||
|
|
cc82697f00 | ||
|
|
e3f4d6dc0d | ||
|
|
b698206d07 | ||
|
|
7451ccbc98 | ||
|
|
6448413adf | ||
|
|
d91f838e3b | ||
|
|
fc820aba6b | ||
|
|
a61dba8ec8 | ||
|
|
025cde8961 | ||
|
|
2f8659a375 | ||
|
|
f1eff7d2f0 | ||
|
|
0d3caa546c | ||
|
|
e7444fd852 | ||
|
|
55ac593102 | ||
|
|
4ed8a7792c | ||
|
|
161f01fa0d | ||
|
|
606151f79d | ||
|
|
b00b1778d1 | ||
|
|
dc4af1ddad | ||
|
|
6b88fdfbbf | ||
|
|
edf712f2f6 | ||
|
|
70f1cf07c5 | ||
|
|
6d6c5d5fad | ||
|
|
3a44a460bd | ||
|
|
8655d4027f | ||
|
|
271e545a49 | ||
|
|
6b97edc9db | ||
|
|
e4f4a7d657 | ||
|
|
861020eeb7 | ||
|
|
0dc20693e2 | ||
|
|
08746292c7 | ||
|
|
e2636505e2 | ||
|
|
2219f48fca | ||
|
|
91c64bf350 | ||
|
|
6f6786e04f | ||
|
|
7974cabb43 | ||
|
|
a8ae8fe83f | ||
|
|
e7cdf9c046 | ||
|
|
93c397dca7 | ||
|
|
c9f2773685 | ||
|
|
652d653736 | ||
|
|
219dff38ad | ||
|
|
c5ca614492 | ||
|
|
0fca58b98e | ||
|
|
5d04cd3808 | ||
|
|
59a0a898e9 | ||
|
|
a6499c3b49 | ||
|
|
310b665160 | ||
|
|
b96b8ac7da | ||
|
|
d20a0e1b8e | ||
|
|
716d417359 | ||
|
|
1b6893ea49 | ||
|
|
fef2ab4904 | ||
|
|
cbf9f79e5f | ||
|
|
4c4c5460bc | ||
|
|
26d5ac3a80 | ||
|
|
f71568bad4 | ||
|
|
a4ac042f27 | ||
|
|
25e248a6c3 | ||
|
|
f43063803b | ||
|
|
9e20c21b35 | ||
|
|
44ab78ade5 | ||
|
|
c88d2eba79 | ||
|
|
b1b51d3070 | ||
|
|
dc8ac7d3dc | ||
|
|
3feb849778 | ||
|
|
bc1009a5f1 | ||
|
|
1f06bbc156 | ||
|
|
09bb1cbf3e | ||
|
|
b66752b4fc | ||
|
|
97b9490845 | ||
|
|
6e7cf80f6e | ||
|
|
d250931aae | ||
|
|
7163fb4dbb | ||
|
|
838f4c6ad2 | ||
|
|
c5e5c38a8a | ||
|
|
9f98c24394 | ||
|
|
86765b1706 | ||
|
|
2585b4667f | ||
|
|
bc4332bbb9 | ||
|
|
a3d75ad5da | ||
|
|
101ba34f28 | ||
|
|
ece2001360 | ||
|
|
3e913fda53 | ||
|
|
ec29392c11 | ||
|
|
536fe6964f | ||
|
|
18be35d9e0 | ||
|
|
c6596bc468 | ||
|
|
00bdc72562 | ||
|
|
0394fd90ad | ||
|
|
f27d62f841 | ||
|
|
874d32eb42 | ||
|
|
8e10f41188 | ||
|
|
ad98f53f3b | ||
|
|
6e0fa73249 | ||
|
|
722f104fa0 | ||
|
|
d55f80d633 | ||
|
|
649a29debe | ||
|
|
dedc446418 | ||
|
|
16c5da5dc5 | ||
|
|
a0f762985e | ||
|
|
f76dd51134 | ||
|
|
103dd93ae4 | ||
|
|
5f216a7bf5 | ||
|
|
dfa6ec0c79 | ||
|
|
e011cbecbb | ||
|
|
c2b349aeb8 | ||
|
|
6f0f7be867 | ||
|
|
bc86249658 | ||
|
|
0ac4d8220c | ||
|
|
1ce484be52 | ||
|
|
899ac2a2ed | ||
|
|
ca6974b66f | ||
|
|
3d966bda8f | ||
|
|
b11ee3fbdb | ||
|
|
3f8b0cae72 | ||
|
|
8261563a10 | ||
|
|
b15ce04e5d | ||
|
|
7a515cd489 | ||
|
|
5b7dafe70a | ||
|
|
eaa47fbf9c | ||
|
|
bbc0c1db3b | ||
|
|
b35a639e33 | ||
|
|
48b8c8a3ff | ||
|
|
5a80359443 | ||
|
|
67163a9fbe | ||
|
|
0d1c90c616 | ||
|
|
3678a4cc07 | ||
|
|
c0a0f34439 | ||
|
|
640ea5e3fa | ||
|
|
cb40c6487b | ||
|
|
1570af44e2 | ||
|
|
d00c9feeac | ||
|
|
b308958bf0 | ||
|
|
9125d0c79f | ||
|
|
ee80782345 | ||
|
|
b5fd6611f2 | ||
|
|
12558c7ef3 | ||
|
|
99733cda7c | ||
|
|
1482689f09 | ||
|
|
a1d3fc19a2 | ||
|
|
103770a995 | ||
|
|
38a2e62603 | ||
|
|
ae41a43567 | ||
|
|
08a7253e99 | ||
|
|
ba6069686f | ||
|
|
6bb521a207 | ||
|
|
fac53a08ca | ||
|
|
d67a92b2b4 | ||
|
|
ffee6d658d | ||
|
|
58f3d7badf | ||
|
|
388e98fba9 | ||
|
|
f883420714 | ||
|
|
d99f7598e1 | ||
|
|
64bfe58e03 | ||
|
|
d05f7b64b4 | ||
|
|
bc036dcaf6 | ||
|
|
64cebdad77 | ||
|
|
e594251208 | ||
|
|
cea26bd150 | ||
|
|
27905a8fc7 | ||
|
|
2f2704b2b6 | ||
|
|
5afa255ab8 | ||
|
|
daa781706a | ||
|
|
88bd798427 | ||
|
|
d77c8b1c56 | ||
|
|
e07f18c172 | ||
|
|
6bc13e8d66 | ||
|
|
25482bc539 | ||
|
|
847e2a24cf | ||
|
|
514933d459 | ||
|
|
04e4f0ffa0 | ||
|
|
109c669056 | ||
|
|
fb68c74143 | ||
|
|
acdf31b40a | ||
|
|
48c611c90b | ||
|
|
f338f50b4d | ||
|
|
f0b119d90a | ||
|
|
e8c268026c | ||
|
|
9387eab814 | ||
|
|
d769abafd9 | ||
|
|
742ab90d21 | ||
|
|
2a346af4e2 | ||
|
|
df5ebc9245 | ||
|
|
71f17d9a2a | ||
|
|
4e7ab5d3e3 | ||
|
|
e5ffe61a07 | ||
|
|
e601bccdfb | ||
|
|
155c9b4154 | ||
|
|
7b0e29baa9 | ||
|
|
0152831319 | ||
|
|
17ce1bbe0f | ||
|
|
db953dbc62 | ||
|
|
65ed77ecd7 | ||
|
|
bdf4dc9ce1 | ||
|
|
c01f3b885e | ||
|
|
538078f6d0 | ||
|
|
2a4be17f56 | ||
|
|
9d647291fa | ||
|
|
595de2e2aa | ||
|
|
778e1f0b76 | ||
|
|
b5910c75f5 | ||
|
|
23cd8772f5 | ||
|
|
202db872de | ||
|
|
934e9d36c3 | ||
|
|
30066aa336 | ||
|
|
cc4db18707 | ||
|
|
30a3b125c8 | ||
|
|
084d8fb72b | ||
|
|
463d44920b | ||
|
|
adfc78f952 | ||
|
|
f533120df6 | ||
|
|
ec37caccfb | ||
|
|
d0b9669c50 | ||
|
|
dfb355495a | ||
|
|
0f618995f4 | ||
|
|
35ead94515 | ||
|
|
6c8ccc1c7e | ||
|
|
361fb49fd0 | ||
|
|
90139d028d | ||
|
|
d03915bde9 | ||
|
|
8a7549fb21 | ||
|
|
6003e6933b | ||
|
|
35671b2f8d | ||
|
|
43686810d2 | ||
|
|
9bd2afee61 | ||
|
|
7bcb9ae39f | ||
|
|
f39eac9b87 | ||
|
|
f370521a55 | ||
|
|
c763218430 | ||
|
|
2df5b97e5e | ||
|
|
0a7e1c5e4c | ||
|
|
2d80fca221 | ||
|
|
3b8baddfba | ||
|
|
5319fe68f0 | ||
|
|
6b11b281c4 | ||
|
|
c79a73e88c | ||
|
|
53a63d6ae2 | ||
|
|
c95ddfe754 | ||
|
|
bfed5a56f0 | ||
|
|
41275f4a8a | ||
|
|
9fcb4f93e4 | ||
|
|
11a7827eef | ||
|
|
b7de9976a6 | ||
|
|
9671ec7c3d | ||
|
|
cb4aec479f | ||
|
|
44f34f9bb2 | ||
|
|
90b6c0902d | ||
|
|
445cfacb10 | ||
|
|
7989498177 | ||
|
|
b38cacbab8 | ||
|
|
4b121c28d0 | ||
|
|
4e1b4b79ef | ||
|
|
18500e679b | ||
|
|
174eb5bcf2 | ||
|
|
8d32ab3f4f | ||
|
|
5cc3a8118f | ||
|
|
753ba2af6a | ||
|
|
757f326c33 | ||
|
|
fc36600a6a | ||
|
|
038bcb91c2 | ||
|
|
0b1714c8f6 | ||
|
|
76141ba703 | ||
|
|
064b2120e4 | ||
|
|
732d03f3af | ||
|
|
81d0f131c4 | ||
|
|
5d0ff02cb3 | ||
|
|
ba41375179 | ||
|
|
fe7f05fc1e | ||
|
|
cbe2b7be32 | ||
|
|
0d1152de1c | ||
|
|
eaf5e06683 | ||
|
|
0921eb8038 | ||
|
|
64c8354511 | ||
|
|
cd21ca7bf4 | ||
|
|
ded925451b | ||
|
|
b0d5be69d0 | ||
|
|
2f7fa79b07 | ||
|
|
4fd5318e52 | ||
|
|
810c508489 | ||
|
|
383a0870f1 | ||
|
|
9e4a00b968 | ||
|
|
a57cce6b94 | ||
|
|
74a561f62e | ||
|
|
52acac2933 | ||
|
|
9830daef91 | ||
|
|
2f9f7670da | ||
|
|
808aaaf053 | ||
|
|
1044d0ad37 | ||
|
|
0ebd9e3bbe | ||
|
|
57d1a2ef9a | ||
|
|
eeacac8749 | ||
|
|
9e1ff7b479 | ||
|
|
32d2f3d2b2 | ||
|
|
c64aff5992 | ||
|
|
3390d66a7e | ||
|
|
e388299f05 | ||
|
|
f31fa5c99f | ||
|
|
fec8d9029c | ||
|
|
c4bc76b219 | ||
|
|
7d91e5f38a | ||
|
|
907ccf6494 | ||
|
|
c7fabe82d6 | ||
|
|
9f9ff6bf09 | ||
|
|
f60c003826 | ||
|
|
6a30f98145 | ||
|
|
7855c8b469 | ||
|
|
ee85afd30c | ||
|
|
09932f11d5 | ||
|
|
85afab5b36 | ||
|
|
75ef6810e2 | ||
|
|
66db5eb88c | ||
|
|
d5c3a4805b | ||
|
|
82c777e7c2 | ||
|
|
cdbc88f92e | ||
|
|
970a019476 | ||
|
|
9c9ba5d6f4 | ||
|
|
86790da2ec | ||
|
|
7fff423649 | ||
|
|
d258228cc0 | ||
|
|
9bf3ffc3ae | ||
|
|
7168f2e411 | ||
|
|
c75ad74ce0 | ||
|
|
7ed8d6feac | ||
|
|
e2bf15a6ce | ||
|
|
29139b104c | ||
|
|
0b2a85f803 | ||
|
|
a55688a95d | ||
|
|
f39ca5e3a7 | ||
|
|
1a2aedcf00 | ||
|
|
05e9c3a6c9 | ||
|
|
0d9ce81d20 | ||
|
|
eed44e0012 | ||
|
|
93073f2700 | ||
|
|
53a08c9101 | ||
|
|
6540059a28 | ||
|
|
48c816f2f9 | ||
|
|
1edb30d1ea | ||
|
|
797c798787 | ||
|
|
2c3341dfdf | ||
|
|
31357eb4e5 | ||
|
|
3401322e6e | ||
|
|
50ed2ba9bc | ||
|
|
ff8bdbcdc6 | ||
|
|
935802da1a | ||
|
|
1dffa3d974 | ||
|
|
78144f5fab | ||
|
|
47aa5dc1df | ||
|
|
bf7a1a205c | ||
|
|
96bd51471d | ||
|
|
bbd9fcca0c | ||
|
|
f263e89f06 | ||
|
|
698ea74132 | ||
|
|
766fc1ece2 | ||
|
|
247dd180ce | ||
|
|
23ed3de93c | ||
|
|
c0791baa3d | ||
|
|
9ad89d134e | ||
|
|
84ce0e3d9f | ||
|
|
92fe0d2e44 | ||
|
|
864ca3d91e | ||
|
|
8729c5f591 | ||
|
|
89be74aab5 | ||
|
|
24c5e367cc | ||
|
|
5004ca4a33 | ||
|
|
799d0ce726 | ||
|
|
47348d9002 | ||
|
|
3eeecf1cc2 | ||
|
|
31380a1976 | ||
|
|
6974764393 | ||
|
|
0e73ba0cc1 | ||
|
|
5bb5a70b0c | ||
|
|
2830ab92f7 | ||
|
|
2eb0e677a8 | ||
|
|
a5f5eccdbe | ||
|
|
82ff8dbf6b | ||
|
|
c9d8fc525c | ||
|
|
3854a4561d | ||
|
|
cf4afeb7f0 | ||
|
|
755e15c3e1 | ||
|
|
7f0d020466 | ||
|
|
5a2be88f41 | ||
|
|
be570117bb | ||
|
|
640fe5722c | ||
|
|
dff1ad01a6 | ||
|
|
e7a00354dc | ||
|
|
f042f7a51a | ||
|
|
4d92a5b433 | ||
|
|
4aed87bf45 | ||
|
|
a5e76e2e4e | ||
|
|
c7277c964e | ||
|
|
846b622c80 | ||
|
|
abcdff3e60 | ||
|
|
4fcfe3a485 | ||
|
|
76b0204d91 | ||
|
|
cac6f0534a | ||
|
|
f66bb568df | ||
|
|
cc4556824e | ||
|
|
c6f7efb797 | ||
|
|
6a2fcb21dc | ||
|
|
2b347c65de | ||
|
|
f1823630be |
No files matched your search
@@ -1,11 +1,8 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/master/gersemi/configuration.schema.json
|
||||
|
||||
color: false
|
||||
definitions: [ares/CMakeLists.txt]
|
||||
line_length: 120
|
||||
indent: 2
|
||||
list_expansion: favour-inlining
|
||||
quiet: false
|
||||
unsafe: false
|
||||
warn_about_unknown_commands: false
|
||||
workers: 10
|
||||
@@ -0,0 +1,32 @@
|
||||
# dummy GitHub target used to enable editing of GitHub files within generated IDE projects
|
||||
|
||||
add_library(.github INTERFACE)
|
||||
|
||||
set(.github_sources
|
||||
ISSUE_TEMPLATE/bug_report.md
|
||||
ISSUE_TEMPLATE/feature_request.md
|
||||
ISSUE_TEMPLATE/game-issue-report.md
|
||||
scripts/build_macos.sh
|
||||
scripts/build_sdl.sh
|
||||
scripts/build_ubuntu.sh
|
||||
scripts/build_windows.sh
|
||||
scripts/package_artifacts.sh
|
||||
workflows/build-aux.yml
|
||||
workflows/build.yml
|
||||
workflows/pr.yml
|
||||
workflows/push.yml
|
||||
workflows/release.yml
|
||||
)
|
||||
|
||||
target_sources(
|
||||
.github
|
||||
PRIVATE
|
||||
${.github_sources}
|
||||
../.gitignore
|
||||
../.gitattributes
|
||||
../README.md
|
||||
../LICENSE
|
||||
)
|
||||
|
||||
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${.github_sources})
|
||||
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# clone the SDL repository
|
||||
git clone https://github.com/libsdl-org/SDL.git
|
||||
# change into the SDL directory
|
||||
pushd SDL
|
||||
mkdir build
|
||||
# build SDL
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build --config Release
|
||||
# install SDL libs and include files to /usr/local
|
||||
sudo cmake --install build --prefix=/usr/local
|
||||
popd
|
||||
@@ -17,11 +17,13 @@ jobs:
|
||||
msystem: mingw64
|
||||
install: make git cmake ccache ninja mingw-w64-x86_64-toolchain
|
||||
target-cmake-preset: windows-ci-mingw-native
|
||||
- name: windows-clang-cl-x64
|
||||
- name: windows-mingw-clang64-x64
|
||||
os: windows-latest
|
||||
windres: rc
|
||||
shell: bash
|
||||
target-cmake-preset: windows-ci-cl-native
|
||||
compiler: clang++
|
||||
shell: 'msys2 {0}'
|
||||
msystem: clang64
|
||||
install: make git cmake ccache ninja mingw-w64-clang-x86_64-clang
|
||||
target-cmake-preset: windows-ci-mingw-native
|
||||
- name: windows-arm64
|
||||
os: windows-11-arm
|
||||
compiler: clang++
|
||||
@@ -56,6 +58,14 @@ jobs:
|
||||
os: ubuntu-24.04-arm
|
||||
shell: bash
|
||||
target-cmake-preset: ubuntu-ci-clang
|
||||
- name: ubuntu-gcc-arm64-qt
|
||||
os: ubuntu-24.04-arm
|
||||
shell: bash
|
||||
target-cmake-preset: ubuntu-ci-qt
|
||||
- name: freebsd
|
||||
os: ubuntu-24.04
|
||||
shell: cpa.sh {0}
|
||||
target-cmake-preset: ubuntu-ci-clang
|
||||
name: ${{ matrix.program }}-${{ matrix.platform.name }}
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
defaults:
|
||||
@@ -78,12 +88,15 @@ jobs:
|
||||
run: |
|
||||
git config core.autocrlf true
|
||||
- name: Install Linux Dependencies
|
||||
if: runner.os == 'Linux'
|
||||
if: runner.os == 'Linux' && matrix.platform.name != 'freebsd'
|
||||
run: |
|
||||
sudo apt-get update -y -qq
|
||||
sudo apt-get install cmake ccache ninja-build libsdl2-dev libgtk-3-dev libao-dev libopenal-dev
|
||||
sudo apt-get install cmake ccache ninja-build libsdl2-dev libgtk-3-dev qt6-base-dev
|
||||
- name: Build SDL3
|
||||
if: runner.os == 'Linux' && matrix.platform.name != 'freebsd'
|
||||
run: .github/scripts/build_sdl.sh
|
||||
- name: "Build: Linux"
|
||||
if: runner.os == 'Linux'
|
||||
if: runner.os == 'Linux' && matrix.platform.name != 'freebsd'
|
||||
run: .github/scripts/build_ubuntu.sh
|
||||
env:
|
||||
TARGET_PRESET: ${{ matrix.platform.target-cmake-preset }}
|
||||
@@ -95,6 +108,23 @@ jobs:
|
||||
CROSS_COMPILE: ${{ matrix.platform.native-cmake-preset != '' }}
|
||||
NATIVE_PRESET: ${{ matrix.platform.native-cmake-preset }}
|
||||
TARGET_PRESET: ${{ matrix.platform.target-cmake-preset }}
|
||||
- name: "Start FreeBSD VM"
|
||||
if: matrix.platform.name == 'freebsd'
|
||||
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641
|
||||
with:
|
||||
operating_system: freebsd
|
||||
version: '15.1'
|
||||
- name: "Install FreeBSD Dependencies"
|
||||
if: matrix.platform.name == 'freebsd'
|
||||
run: |
|
||||
sudo pkg upgrade -y
|
||||
sudo pkg install -y cmake-core pkgconf ccache ninja gtk3 sdl3 librashader
|
||||
- name: "Build: FreeBSD"
|
||||
if: matrix.platform.name == 'freebsd'
|
||||
env:
|
||||
TARGET_PRESET: ${{ matrix.platform.target-cmake-preset }}
|
||||
shell: cpa.sh {0} --environment-variables TARGET_PRESET
|
||||
run: .github/scripts/build_ubuntu.sh
|
||||
- name: "Compress Build Artifacts (Windows)"
|
||||
if: runner.os != 'macOS' && runner.os != 'Linux'
|
||||
run: |
|
||||
|
||||
@@ -24,11 +24,9 @@ jobs:
|
||||
platform:
|
||||
- name: windows-x64
|
||||
os: windows-latest
|
||||
compiler: clang++
|
||||
shell: 'msys2 {0}'
|
||||
msystem: clang64
|
||||
install: make git cmake ccache ninja mingw-w64-clang-x86_64-clang
|
||||
target-cmake-preset: windows-ci-mingw-native
|
||||
windres: rc
|
||||
shell: bash
|
||||
target-cmake-preset: windows-ci-cl-native
|
||||
- name: windows-clang-cl-arm64
|
||||
os: windows-latest
|
||||
windres: rc
|
||||
@@ -77,6 +75,7 @@ jobs:
|
||||
- name: Install macOS Dependencies
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew uninstall --ignore-dependencies cmake
|
||||
brew install ${{ matrix.platform.install }}
|
||||
- name: "Build: Windows"
|
||||
if: runner.os != 'macOS' && runner.os != 'Linux'
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
name: Release
|
||||
permissions:
|
||||
contents: write
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
|
||||
@@ -41,6 +41,9 @@ desktop-ui/resource/resource.hpp
|
||||
mia/resource/resource.cpp
|
||||
mia/resource/resource.hpp
|
||||
|
||||
# Exclude Qt generated MOC file
|
||||
hiro/qt/qt.moc
|
||||
|
||||
# Exclude fetched third-party processor test binaries
|
||||
tests/arm7tdmi/tests
|
||||
tests/m68000/tests
|
||||
|
||||
+10
-8
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.28...4.0)
|
||||
cmake_minimum_required(VERSION 3.28...4.1)
|
||||
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/bootstrap.cmake" NO_POLICY_SCOPE)
|
||||
|
||||
@@ -8,8 +8,6 @@ include(compilerconfig)
|
||||
include(defaults)
|
||||
include(helpers)
|
||||
|
||||
add_subdirectory(cmake)
|
||||
|
||||
add_subdirectory(thirdparty)
|
||||
|
||||
add_subdirectory(nall/nall)
|
||||
@@ -20,8 +18,8 @@ add_subdirectory(hiro)
|
||||
# gersemi: off
|
||||
set(
|
||||
ARES_CORES
|
||||
a26 fc sfc sg ms md ps1 pce ng msx cv myvision gb gba ws ngp spec n64
|
||||
CACHE STRING LIST
|
||||
a26 a52 fc sfc sg ms md ps1 pce ng msx cv myvision gb gba ws ngp spec n64
|
||||
CACHE STRING "List of systems to be built by the project"
|
||||
)
|
||||
# gersemi: on
|
||||
|
||||
@@ -32,24 +30,28 @@ add_subdirectory(desktop-ui)
|
||||
option(ARES_BUILD_OPTIONAL_TARGETS "Include supplemental tools and tests" OFF)
|
||||
|
||||
if(ARES_BUILD_OPTIONAL_TARGETS)
|
||||
enable_testing()
|
||||
add_subdirectory(tests/arm7tdmi)
|
||||
add_subdirectory(tests/armv6m)
|
||||
add_subdirectory(tests/i8080)
|
||||
add_subdirectory(tests/m68000)
|
||||
if(NOT OS_WINDOWS AND NOT OS_MACOS)
|
||||
if(HIRO_BACKEND STREQUAL "GTK3")
|
||||
add_subdirectory(tools/genius)
|
||||
else()
|
||||
target_disable_subproject(genius "genius (database editor)")
|
||||
endif()
|
||||
add_subdirectory(tools/mame2bml)
|
||||
else()
|
||||
target_disable_subproject(arm7tdmi "arm7tdmi processor test harness")
|
||||
target_disable_subproject(armv6m "armv6m processor test harness")
|
||||
target_disable_subproject(i8080 "i8080 processor test harness")
|
||||
target_disable_subproject(m68000 "m68000 processor test harness")
|
||||
target_disable_subproject(mame2bml "mame2bml (MAME manifest converter)")
|
||||
target_disable_subproject(genius "genius (database editor)")
|
||||
endif()
|
||||
|
||||
add_subdirectory(tools/sourcery)
|
||||
|
||||
message_configuration()
|
||||
|
||||
add_subdirectory(.github)
|
||||
add_subdirectory(cmake)
|
||||
return()
|
||||
+20
-8
@@ -46,7 +46,7 @@
|
||||
"rhs": "Windows"
|
||||
},
|
||||
"binaryDir": "${sourceDir}/build_vsclang",
|
||||
"generator": "Visual Studio 17 2022",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"toolset": "ClangCL",
|
||||
"cacheVariables": {
|
||||
"ENABLE_CCACHE": true
|
||||
@@ -63,7 +63,7 @@
|
||||
"rhs": "Windows"
|
||||
},
|
||||
"binaryDir": "${sourceDir}/build_msvc",
|
||||
"generator": "Visual Studio 17 2022",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"cacheVariables": {
|
||||
"ENABLE_CCACHE": true
|
||||
}
|
||||
@@ -82,7 +82,7 @@
|
||||
"binaryDir": "${sourceDir}/build",
|
||||
"cacheVariables": {
|
||||
"CMAKE_OSX_ARCHITECTURES": {"type": "STRING", "value": "x86_64;arm64"},
|
||||
"CMAKE_OSX_DEPLOYMENT_TARGET": {"type": "STRING", "value": "10.15"},
|
||||
"CMAKE_OSX_DEPLOYMENT_TARGET": {"type": "STRING", "value": "11.0"},
|
||||
"ARES_CODESIGN_IDENTITY": {"type": "STRING", "value": "$penv{MACOS_CERTIFICATE_NAME}"},
|
||||
"ARES_CODESIGN_TEAM": {"type": "STRING", "value": "$penv{MACOS_NOTARIZATION_TEAMID}"},
|
||||
"ENABLE_CCACHE": true,
|
||||
@@ -103,7 +103,8 @@
|
||||
"ARES_BUILD_OPTIONAL_TARGETS": true,
|
||||
"ARES_UNITY_CORES": true,
|
||||
"ARES_PRECOMPILE_HEADERS": true,
|
||||
"CMAKE_MSVC_RUNTIME_LIBRARY": {"type": "STRING", "value": "MultiThreaded"}
|
||||
"CMAKE_MSVC_RUNTIME_LIBRARY": {"type": "STRING", "value": "MultiThreaded"},
|
||||
"CMAKE_IGNORE_PATH": {"type": "STRING", "value": "C:/Strawberry/c/bin/"}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -117,7 +118,7 @@
|
||||
"name": "windows-ci-cl-native",
|
||||
"displayName": "Windows for CI (x64 ClangCL)",
|
||||
"description": "Single-arch binary for building on Github Actions",
|
||||
"generator": "Visual Studio 17 2022",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"inherits": ["windows-ci-base"],
|
||||
"toolset": "ClangCL"
|
||||
},
|
||||
@@ -126,7 +127,7 @@
|
||||
"displayName": "Windows for CI (arm64 ClangCL)",
|
||||
"description": "Single-arch binary for building on Github Actions",
|
||||
"inherits": ["windows-ci-base"],
|
||||
"generator": "Visual Studio 17 2022",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": "arm64",
|
||||
"toolset": "ClangCL",
|
||||
"cacheVariables": {
|
||||
@@ -137,14 +138,14 @@
|
||||
"name": "windows-ci-msvc-native",
|
||||
"displayName": "Windows for CI (x64 MSVC)",
|
||||
"description": "Single-arch binary for building on Github Actions",
|
||||
"generator": "Visual Studio 17 2022",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"inherits": ["windows-ci-base"]
|
||||
},
|
||||
{
|
||||
"name": "windows-ci-msvc-cross",
|
||||
"displayName": "Windows for CI (arm64 MSVC)",
|
||||
"description": "Single-arch binary for building on Github Actions",
|
||||
"generator": "Visual Studio 17 2022",
|
||||
"generator": "Visual Studio 18 2026",
|
||||
"architecture": "arm64",
|
||||
"inherits": ["windows-ci-base"],
|
||||
"cacheVariables": {
|
||||
@@ -160,6 +161,7 @@
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": {"type": "STRING", "value": "RelWithDebInfo"},
|
||||
"ENABLE_CCACHE": true,
|
||||
"ENABLE_IPO": false,
|
||||
"ARES_BUILD_LOCAL": false,
|
||||
"ARES_BUILD_OPTIONAL_TARGETS": true,
|
||||
"ARES_PRECOMPILE_HEADERS": false
|
||||
@@ -174,6 +176,16 @@
|
||||
"CMAKE_C_COMPILER": "clang",
|
||||
"CMAKE_CXX_COMPILER": "clang++"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ubuntu-ci-qt",
|
||||
"displayName": "Ubuntu CI (arm64, gcc, Qt)",
|
||||
"description": "Single-arch binary for building on Github Actions",
|
||||
"inherits": ["ubuntu-ci"],
|
||||
"cacheVariables": {
|
||||
"USE_QT6": true,
|
||||
"ARES_BUILD_OPTIONAL_TARGETS": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
|
||||
@@ -16,6 +16,40 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
----------------------------------------------------------------------
|
||||
|
||||
----------------------------------------------------------------------
|
||||
MAME-derived ROM decryption and arcade database
|
||||
|
||||
Portions of the ROM decryption code are derived from MAME source files
|
||||
licensed under the BSD 3-Clause License. The arcade database is generated
|
||||
from MAME machine data.
|
||||
|
||||
Copyright (c) 1997-2026 MAMEdev and contributors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
----------------------------------------------------------------------
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Stack-less Just-In-Time compiler
|
||||
|
||||
|
||||
@@ -38,12 +38,18 @@ When started from the command-line, ares accepts a few options.
|
||||
Usage: ./ares [options] game(s)
|
||||
|
||||
--help Displays available options and exit
|
||||
--version Displays the version string of the application
|
||||
--terminal Create new terminal window (Windows only)
|
||||
--fullscreen Start in full screen mode
|
||||
--pseudofullscreen Start in pseudo full screen mode
|
||||
--kiosk Start in minimal UI mode (implies --no-file-prompt)
|
||||
--system system Specify the system name
|
||||
--shader shader Specify a slang shader to load (requires OpenGL or Metal)
|
||||
--setting name=value Specify a value for a setting
|
||||
--dump-all-settings Show a list of all existing settings and exit
|
||||
--no-file-prompt Do not prompt to load (optional) additional roms (eg: 64DD)
|
||||
--settings-file path Specify a settings file override (settings.bml)
|
||||
--save-state slot Specify a save state slot to load (1-9)
|
||||
```
|
||||
|
||||
The --system option is useful when the system type cannot be auto-detected.
|
||||
|
||||
+20
-1
@@ -21,7 +21,7 @@ if(ARES_PRECOMPILE_HEADERS)
|
||||
target_precompile_headers(ares PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:ares/ares.hpp>")
|
||||
endif()
|
||||
|
||||
target_link_libraries(ares PUBLIC libco sljit nall)
|
||||
target_link_libraries(ares PRIVATE libco ares::nall::headers $<$<BOOL:${ARES_ENABLE_CHD}>:chdr-static> PUBLIC sljit)
|
||||
|
||||
if(ARES_PROFILE_ACCURACY)
|
||||
target_compile_definitions(ares PUBLIC PROFILE_ACCURACY)
|
||||
@@ -99,6 +99,18 @@ target_sources(
|
||||
ares/node/video/video.hpp
|
||||
)
|
||||
|
||||
target_sources(
|
||||
ares
|
||||
PRIVATE
|
||||
ares/node/debugger/debugger.hpp
|
||||
ares/node/debugger/graphics.hpp
|
||||
ares/node/debugger/memory.hpp
|
||||
ares/node/debugger/properties.hpp
|
||||
ares/node/debugger/tracer/tracer.hpp
|
||||
ares/node/debugger/tracer/instruction.hpp
|
||||
ares/node/debugger/tracer/notification.hpp
|
||||
)
|
||||
|
||||
if(OS_WINDOWS)
|
||||
include(cmake/os-windows.cmake)
|
||||
elseif(OS_MACOS)
|
||||
@@ -140,6 +152,13 @@ else()
|
||||
disable_core("Atari 2600")
|
||||
endif()
|
||||
|
||||
if(a52 IN_LIST ARES_CORES)
|
||||
enable_core("Atari 5200")
|
||||
add_subdirectory(a52)
|
||||
else()
|
||||
disable_core("Atari 5200")
|
||||
endif()
|
||||
|
||||
if(fc IN_LIST ARES_CORES)
|
||||
enable_core("NES / Famicom")
|
||||
add_subdirectory(fc)
|
||||
|
||||
+77
-2
@@ -1,4 +1,4 @@
|
||||
ares_components(mos6502)
|
||||
ares_components(arm7tdmi armv6m m24c mos6502)
|
||||
|
||||
ares_add_sources(
|
||||
CORE #
|
||||
@@ -10,6 +10,9 @@ ares_add_sources(
|
||||
controller/controller.cpp
|
||||
cartridge/cartridge.cpp
|
||||
cpu/cpu.cpp
|
||||
elf/elf.cpp
|
||||
harmony/harmony.cpp
|
||||
video/video.cpp
|
||||
tia/tia.cpp
|
||||
riot/riot.cpp
|
||||
)
|
||||
@@ -19,6 +22,7 @@ ares_add_sources(
|
||||
a26
|
||||
INCLUDED #
|
||||
a26.hpp
|
||||
persistent-memory.hpp
|
||||
CMakeLists.txt
|
||||
)
|
||||
|
||||
@@ -36,11 +40,48 @@ ares_add_sources(
|
||||
cartridge/board/board.cpp
|
||||
cartridge/board/board.hpp
|
||||
cartridge/board/commavid.cpp
|
||||
cartridge/board/cdf.cpp
|
||||
cartridge/board/bus.cpp
|
||||
cartridge/board/chetiry.cpp
|
||||
cartridge/board/fa2.cpp
|
||||
cartridge/board/dpc-plus.cpp
|
||||
cartridge/board/elf.cpp
|
||||
cartridge/board/linear.cpp
|
||||
cartridge/board/movie-cart.cpp
|
||||
cartridge/board/parker-bros.cpp
|
||||
cartridge/board/tigervision.cpp
|
||||
)
|
||||
|
||||
ares_add_sources(
|
||||
CORE #
|
||||
a26
|
||||
INCLUDED #
|
||||
movie-cart/format.hpp
|
||||
)
|
||||
|
||||
ares_add_sources(
|
||||
CORE #
|
||||
a26
|
||||
INCLUDED #
|
||||
elf/elf.hpp
|
||||
elf/queue.cpp
|
||||
elf/parser.cpp
|
||||
elf/linker.cpp
|
||||
elf/memory.cpp
|
||||
elf/vcslib.cpp
|
||||
elf/runtime.cpp
|
||||
)
|
||||
|
||||
ares_add_sources(
|
||||
CORE #
|
||||
a26
|
||||
INCLUDED #
|
||||
harmony/harmony.hpp
|
||||
harmony/memory.cpp
|
||||
harmony/serialization.cpp
|
||||
harmony/timing.cpp
|
||||
)
|
||||
|
||||
ares_add_sources(
|
||||
CORE #
|
||||
a26
|
||||
@@ -50,6 +91,25 @@ ares_add_sources(
|
||||
controller/port.hpp
|
||||
controller/gamepad/gamepad.cpp
|
||||
controller/gamepad/gamepad.hpp
|
||||
controller/booster-grip/booster-grip.cpp
|
||||
controller/booster-grip/booster-grip.hpp
|
||||
controller/sega-genesis/sega-genesis.cpp
|
||||
controller/sega-genesis/sega-genesis.hpp
|
||||
controller/joy2b-plus/joy2b-plus.cpp
|
||||
controller/joy2b-plus/joy2b-plus.hpp
|
||||
controller/savekey/savekey.cpp
|
||||
controller/savekey/savekey.hpp
|
||||
controller/atarivox/atarivox.cpp
|
||||
controller/atarivox/atarivox.hpp
|
||||
controller/atarivox/speakjet.cpp
|
||||
controller/atarivox/speakjet.hpp
|
||||
controller/kidvid/audio.cpp
|
||||
controller/kidvid/audio.hpp
|
||||
controller/kidvid/kidvid.cpp
|
||||
controller/kidvid/kidvid.hpp
|
||||
controller/kidvid/tables.hpp
|
||||
controller/quadtari/quadtari.cpp
|
||||
controller/quadtari/quadtari.hpp
|
||||
)
|
||||
|
||||
ares_add_sources(
|
||||
@@ -81,18 +141,33 @@ ares_add_sources(
|
||||
system/system.hpp
|
||||
)
|
||||
|
||||
ares_add_sources(
|
||||
CORE #
|
||||
a26
|
||||
INCLUDED #
|
||||
video/color.cpp
|
||||
video/receiver.cpp
|
||||
video/serialization.cpp
|
||||
video/video.hpp
|
||||
)
|
||||
|
||||
ares_add_sources(
|
||||
CORE #
|
||||
a26
|
||||
INCLUDED #
|
||||
tia/audio.cpp
|
||||
tia/analog.cpp
|
||||
tia/ball.cpp
|
||||
tia/color.cpp
|
||||
tia/collision.cpp
|
||||
tia/io.cpp
|
||||
tia/missile.cpp
|
||||
tia/objects.cpp
|
||||
tia/player.cpp
|
||||
tia/playfield.cpp
|
||||
tia/priority.cpp
|
||||
tia/serialization.cpp
|
||||
tia/timing.cpp
|
||||
tia/tia.hpp
|
||||
tia/trigger.cpp
|
||||
tia/write-queue.cpp
|
||||
)
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
|
||||
#include <a26/cartridge/cartridge.cpp>
|
||||
#include <a26/controller/controller.cpp>
|
||||
#include <a26/cpu/cpu.cpp>
|
||||
#include <a26/elf/elf.cpp>
|
||||
#include <a26/harmony/harmony.cpp>
|
||||
#include <a26/video/video.cpp>
|
||||
#include <a26/riot/riot.cpp>
|
||||
#include <a26/system/system.cpp>
|
||||
#include <a26/tia/tia.cpp>
|
||||
+11
-2
@@ -1,12 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <ares/ares.hpp>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include <component/processor/mos6502/mos6502.hpp>
|
||||
#include <component/processor/arm7tdmi/arm7tdmi.hpp>
|
||||
#include <component/processor/armv6m/armv6m.hpp>
|
||||
#include <component/eeprom/m24c/m24c.hpp>
|
||||
|
||||
namespace ares::Atari2600 {
|
||||
#include <ares/inline.hpp>
|
||||
auto enumerate() -> vector<string>;
|
||||
auto enumerate() -> std::vector<string>;
|
||||
auto load(Node::System& node, string name) -> bool;
|
||||
|
||||
struct Region {
|
||||
@@ -15,11 +20,15 @@ namespace ares::Atari2600 {
|
||||
static inline auto SECAM() -> bool;
|
||||
};
|
||||
|
||||
#include <a26/movie-cart/format.hpp>
|
||||
#include <a26/persistent-memory.hpp>
|
||||
#include <a26/controller/controller.hpp>
|
||||
#include <a26/system/system.hpp>
|
||||
#include <a26/elf/elf.hpp>
|
||||
#include <a26/harmony/harmony.hpp>
|
||||
#include <a26/cartridge/cartridge.hpp>
|
||||
#include <a26/cpu/cpu.hpp>
|
||||
#include <a26/video/video.hpp>
|
||||
#include <a26/tia/tia.hpp>
|
||||
#include <a26/riot/riot.hpp>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
struct Activision8k : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
n1 bank;
|
||||
bool lastAccessWasFE;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto save() -> void override {
|
||||
}
|
||||
|
||||
auto unload() -> void override {
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
if(address.bit(12)) data = rom.read((bank * 0x1000) + (address & 0xfff));
|
||||
access(address, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
access(address, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
bank = 0;
|
||||
lastAccessWasFE = false;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
s(lastAccessWasFE);
|
||||
}
|
||||
|
||||
private:
|
||||
auto access(n16 address, n8 data) -> void {
|
||||
if(lastAccessWasFE) {
|
||||
//Released 8 KiB FE cartridges ignore D7-D6 and invert D5 to select one of two banks.
|
||||
bank = data.bit(5) ^ 1;
|
||||
lastAccessWasFE = false;
|
||||
return;
|
||||
}
|
||||
lastAccessWasFE = address == 0x01fe;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
struct AmigaFC : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
u32 bankCount = 0;
|
||||
u8 activeBank = 0;
|
||||
u8 targetBank = 0;
|
||||
|
||||
auto load() -> void override {
|
||||
if(!Interface::load(rom, "program.rom")) return;
|
||||
bankCount = rom.size() / 4_KiB;
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(address == 0x1ffc) activeBank = targetBank % bankCount;
|
||||
if(address.bit(12)) return rom.read(activeBank * 0x1000 + (address & 0xfff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(address == 0x1ff8) {
|
||||
targetBank = data & 3;
|
||||
} else if(address == 0x1ff9) {
|
||||
auto highBank = (u32)data << 2;
|
||||
if(highBank < bankCount) {
|
||||
targetBank += highBank;
|
||||
targetBank %= bankCount;
|
||||
} else {
|
||||
targetBank = data % bankCount;
|
||||
}
|
||||
} else if(address == 0x1ffc) {
|
||||
activeBank = targetBank % bankCount;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
activeBank = 0;
|
||||
targetBank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(activeBank);
|
||||
s(targetBank);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
struct Atari32In1 : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
n5 game;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
if(address.bit(12)) return rom.read(game * 0x800 + (address & 0x07ff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
//MAME's verified software-list contract cycles 32 2 KiB games on machine reset.
|
||||
//Stella instead presents the same image as frontend-selected slices; no PCB or schematic
|
||||
//presently proves the physical counter or latch behavior.
|
||||
if(reset) game++;
|
||||
else game = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(game);
|
||||
}
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
struct Atari16k : Interface {
|
||||
using Interface::Interface;
|
||||
Atari16k(Cartridge& cartridge, bool hasSaraRam = false) : Interface(cartridge), saraRam(hasSaraRam) {}
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
SaraRam saraRam;
|
||||
n2 bank;
|
||||
bool hasRam;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
@@ -15,44 +14,37 @@ struct Atari16k : Interface {
|
||||
auto unload() -> void override {
|
||||
}
|
||||
|
||||
auto read(n16 address) -> n8 override {
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
if(address == 0x1ff6) bank = 0;
|
||||
if(address == 0x1ff7) bank = 1;
|
||||
if(address == 0x1ff8) bank = 2;
|
||||
if(address == 0x1ff9) bank = 3;
|
||||
|
||||
if(address.bit(12)) {
|
||||
if(hasRam && address >= 0x1080 && address <= 0x10ff) return ram.read(address & 0x7f);
|
||||
if(saraRam.readable(address)) return saraRam.read(address, data);
|
||||
return rom.read((bank * 0x1000) + (address & 0xfff));
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> bool override {
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
if(address == 0x1ff6) bank = 0;
|
||||
if(address == 0x1ff7) bank = 1;
|
||||
if(address == 0x1ff8) bank = 2;
|
||||
if(address == 0x1ff9) bank = 3;
|
||||
|
||||
if(address >= 0x1000 && address <= 0x107f) {
|
||||
hasRam = true;
|
||||
ram.write(address & 0x7f, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
saraRam.write(address, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power() -> void override {
|
||||
auto power(bool reset) -> void override {
|
||||
bank = 0;
|
||||
hasRam = 0;
|
||||
ram.allocate(128);
|
||||
saraRam.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
s(hasRam);
|
||||
s(ram);
|
||||
saraRam.serialize(s);
|
||||
}
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
struct Atari32k : Interface {
|
||||
using Interface::Interface;
|
||||
Atari32k(Cartridge& cartridge, bool hasSaraRam = false) : Interface(cartridge), saraRam(hasSaraRam) {}
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
SaraRam saraRam;
|
||||
n3 bank;
|
||||
bool hasRam;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
@@ -15,7 +14,7 @@ struct Atari32k : Interface {
|
||||
auto unload() -> void override {
|
||||
}
|
||||
|
||||
auto read(n16 address) -> n8 override {
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
if(address == 0x1ff4) bank = 0;
|
||||
if(address == 0x1ff5) bank = 1;
|
||||
if(address == 0x1ff6) bank = 2;
|
||||
@@ -26,14 +25,14 @@ struct Atari32k : Interface {
|
||||
if(address == 0x1ffb) bank = 7;
|
||||
|
||||
if(address.bit(12)) {
|
||||
if(hasRam && address >= 0x1080 && address <= 0x10ff) return ram.read(address & 0x7f);
|
||||
if(saraRam.readable(address)) return saraRam.read(address, data);
|
||||
return rom.read((bank * 0x1000) + (address & 0xfff));
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> bool override {
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
if(address == 0x1ff4) bank = 0;
|
||||
if(address == 0x1ff5) bank = 1;
|
||||
if(address == 0x1ff6) bank = 2;
|
||||
@@ -43,24 +42,17 @@ struct Atari32k : Interface {
|
||||
if(address == 0x1ffa) bank = 6;
|
||||
if(address == 0x1ffb) bank = 7;
|
||||
|
||||
if(address >= 0x1000 && address <= 0x107f) {
|
||||
hasRam = true;
|
||||
ram.write(address & 0x7f, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
saraRam.write(address, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power() -> void override {
|
||||
auto power(bool reset) -> void override {
|
||||
bank = 0;
|
||||
hasRam = 0;
|
||||
ram.allocate(128);
|
||||
saraRam.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
s(hasRam);
|
||||
s(ram);
|
||||
saraRam.serialize(s);
|
||||
}
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
struct Atari8k : Interface {
|
||||
using Interface::Interface;
|
||||
Atari8k(Cartridge& cartridge, bool hasSaraRam = false) : Interface(cartridge), saraRam(hasSaraRam) {}
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
SaraRam saraRam;
|
||||
n1 bank;
|
||||
bool hasRam;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
@@ -15,40 +14,33 @@ struct Atari8k : Interface {
|
||||
auto unload() -> void override {
|
||||
}
|
||||
|
||||
auto read(n16 address) -> n8 override {
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
if(address == 0x1ff8) bank = 0;
|
||||
if(address == 0x1ff9) bank = 1;
|
||||
|
||||
if(address.bit(12)) {
|
||||
if(hasRam && address >= 0x1080 && address <= 0x10ff) return ram.read(address & 0x7f);
|
||||
if(saraRam.readable(address)) return saraRam.read(address, data);
|
||||
return rom.read((bank * 0x1000) + (address & 0xfff));
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> bool override {
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
if(address == 0x1ff8) bank = 0;
|
||||
if(address == 0x1ff9) bank = 1;
|
||||
|
||||
if(address >= 0x1000 && address <= 0x107f) {
|
||||
hasRam = true;
|
||||
ram.write(address & 0x7f, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
saraRam.write(address, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power() -> void override {
|
||||
bank = 0;
|
||||
hasRam = 0;
|
||||
ram.allocate(128);
|
||||
auto power(bool reset) -> void override {
|
||||
bank = 1;
|
||||
saraRam.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
s(hasRam);
|
||||
s(ram);
|
||||
saraRam.serialize(s);
|
||||
}
|
||||
};
|
||||
@@ -1,12 +1,45 @@
|
||||
namespace Board {
|
||||
|
||||
#include "sara-ram.cpp"
|
||||
#include "linear.cpp"
|
||||
#include "activision.cpp"
|
||||
#include "atari8k.cpp"
|
||||
#include "atari16k.cpp"
|
||||
#include "atari32k.cpp"
|
||||
#include "commavid.cpp"
|
||||
#include "parker-bros.cpp"
|
||||
#include "parker-bros-03e0.cpp"
|
||||
#include "jvp.cpp"
|
||||
#include "tigervision.cpp"
|
||||
#include "ua.cpp"
|
||||
#include "cbs-ram-plus.cpp"
|
||||
#include "m-network.cpp"
|
||||
#include "amiga-fc.cpp"
|
||||
#include "wickstead.cpp"
|
||||
#include "jane.cpp"
|
||||
#include "dpc.cpp"
|
||||
#include "mega-boy.cpp"
|
||||
#include "atari-32-in-1.cpp"
|
||||
|
||||
//Homebrew
|
||||
#include "dpc-plus.cpp"
|
||||
#include "cdf.cpp"
|
||||
#include "bus.cpp"
|
||||
#include "chetiry.cpp"
|
||||
#include "fa2.cpp"
|
||||
#include "elf.cpp"
|
||||
#include "movie-cart.cpp"
|
||||
#include "cpuwiz-4ksc.cpp"
|
||||
#include "three-e.cpp"
|
||||
#include "three-ex.cpp"
|
||||
#include "three-e-plus.cpp"
|
||||
#include "enhanced-3f.cpp"
|
||||
#include "four-a50.cpp"
|
||||
#include "ef.cpp"
|
||||
#include "mdm.cpp"
|
||||
#include "x07.cpp"
|
||||
#include "econo-banking.cpp"
|
||||
#include "superbanking.cpp"
|
||||
|
||||
auto Interface::load(Memory::Readable<n8>& memory, string name) -> bool {
|
||||
if(auto fp = pak->read(name)) {
|
||||
@@ -34,4 +67,35 @@ auto Interface::save(Memory::Writable<n8>& memory, string name) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto Interface::readARMMemory(Memory::Readable<n8>& rom, Memory::Writable<n8>& ram,
|
||||
u32 mode, n32 address, u32 romBase, u32 ramLimit, n32& data) -> Harmony::Access {
|
||||
u32 size = mode & ARM7TDMI::Byte ? 1 : mode & ARM7TDMI::Half ? 2 : 4;
|
||||
u32 location = address;
|
||||
auto read = [&](auto& memory, u32 offset) -> Harmony::Access {
|
||||
if(offset > memory.size() || size > memory.size() - offset) return Harmony::Access::Fault;
|
||||
data = 0;
|
||||
for(u32 byte : range(size)) data |= memory.read(offset + byte) << byte * 8;
|
||||
return Harmony::Access::Granted;
|
||||
};
|
||||
|
||||
if(location >= romBase && location < rom.size()) return read(rom, location);
|
||||
if(location >= 0x40000000 && location - 0x40000000 < ramLimit) {
|
||||
return read(ram, location - 0x40000000);
|
||||
}
|
||||
return Harmony::Access::Unmapped;
|
||||
}
|
||||
|
||||
auto Interface::writeARMMemory(Memory::Writable<n8>& ram, u32 mode, n32 address,
|
||||
u32 ramLimit, n32 data) -> Harmony::Access {
|
||||
u32 size = mode & ARM7TDMI::Byte ? 1 : mode & ARM7TDMI::Half ? 2 : 4;
|
||||
u32 location = address;
|
||||
if(location < 0x40000000 || location - 0x40000000 >= ramLimit) return Harmony::Access::Unmapped;
|
||||
auto offset = location - 0x40000000;
|
||||
if(offset > ram.size() || size > ram.size() - offset || size > ramLimit - offset) {
|
||||
return Harmony::Access::Fault;
|
||||
}
|
||||
for(u32 byte : range(size)) ram.write(offset + byte, data >> byte * 8);
|
||||
return Harmony::Access::Granted;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,15 +8,30 @@ struct Interface {
|
||||
virtual auto load() -> void {}
|
||||
virtual auto save() -> void {}
|
||||
virtual auto unload() -> void {}
|
||||
virtual auto read(n16 address) -> n8 { return 0xff; }
|
||||
virtual auto write(n16 address, n8 data) -> bool { return false; }
|
||||
virtual auto power() -> void {}
|
||||
virtual auto read(n16 address, n8 data) -> n8 { return data; }
|
||||
virtual auto write(n16 address, n8 data) -> n8 { return data; }
|
||||
virtual auto power(bool reset) -> void {}
|
||||
virtual auto serialize(serializer&) -> void {}
|
||||
|
||||
virtual auto armInvocation() const -> Harmony::Invocation { return {}; }
|
||||
virtual auto readARM(u32 mode, n32 address, n32& data) -> Harmony::Access {
|
||||
return Harmony::Access::Unmapped;
|
||||
}
|
||||
virtual auto writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access {
|
||||
return Harmony::Access::Unmapped;
|
||||
}
|
||||
virtual auto trapARM(u32 address, n32& value, n32 argument) -> bool { return false; }
|
||||
virtual auto stepARM(u32 clocks) -> void {}
|
||||
|
||||
auto load(Memory::Readable<n8>&, string name) -> bool;
|
||||
auto load(Memory::Writable<n8>&, string name) -> bool;
|
||||
auto save(Memory::Writable<n8>&, string name) -> bool;
|
||||
|
||||
auto readARMMemory(Memory::Readable<n8>& rom, Memory::Writable<n8>& ram,
|
||||
u32 mode, n32 address, u32 romBase, u32 ramLimit, n32& data) -> Harmony::Access;
|
||||
auto writeARMMemory(Memory::Writable<n8>& ram, u32 mode, n32 address,
|
||||
u32 ramLimit, n32 data) -> Harmony::Access;
|
||||
|
||||
Cartridge& cartridge;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
struct BUS : Interface {
|
||||
using Interface::Interface;
|
||||
|
||||
enum class Revision : u32 { BUS0, BUS1, BUS2, BUS3 };
|
||||
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
Revision revision = Revision::BUS0;
|
||||
n3 bank;
|
||||
n8 mode;
|
||||
n2 fastJumpActive;
|
||||
n16 styOperand;
|
||||
n16 busOverdriveAddress;
|
||||
n16 jumpOperand;
|
||||
n32 musicCounter[3];
|
||||
n32 musicFrequency[3];
|
||||
n8 waveformShift[3];
|
||||
u16 datastreamBase = 0;
|
||||
u16 incrementBase = 0;
|
||||
u16 mapBase = 0;
|
||||
u16 waveformBase = 0;
|
||||
u32 audioPhase = 0;
|
||||
|
||||
static constexpr u8 CommunicationStream = 0x10;
|
||||
static constexpr u8 JumpStream = 0x11;
|
||||
|
||||
template<typename MemoryType>
|
||||
auto read32(u32 address, const MemoryType& memory) const -> u32 {
|
||||
if(address + 3 >= memory.size()) return 0;
|
||||
return memory.read(address + 0) << 0 | memory.read(address + 1) << 8
|
||||
| memory.read(address + 2) << 16 | memory.read(address + 3) << 24;
|
||||
}
|
||||
|
||||
auto write32(u32 address, u32 data) -> void {
|
||||
if(address + 3 >= ram.size()) return;
|
||||
for(u32 byte : range(4)) ram.write(address + byte, data >> byte * 8);
|
||||
}
|
||||
|
||||
auto scan(u32 value) const -> u32 {
|
||||
for(u32 offset = 0; offset + 3 < min<u32>(rom.size(), 3_KiB); offset += 4) {
|
||||
if(read32(offset, rom) == value) return offset;
|
||||
}
|
||||
return ~0u;
|
||||
}
|
||||
|
||||
auto detectRevision() -> void {
|
||||
auto signature = scan(0x00535542);
|
||||
if(signature == 0x07f4) revision = Revision::BUS1;
|
||||
else if(signature == 0x0778) revision = Revision::BUS2;
|
||||
else if(signature == 0x0770) revision = Revision::BUS3;
|
||||
else revision = Revision::BUS0;
|
||||
|
||||
if(revision == Revision::BUS0) {
|
||||
datastreamBase = 0x0ae0;
|
||||
incrementBase = 0x0b20;
|
||||
mapBase = 0x0b64;
|
||||
waveformBase = 0;
|
||||
} else {
|
||||
datastreamBase = revision == Revision::BUS3 ? 0x06d8 : 0x06e0;
|
||||
incrementBase = 0x0720;
|
||||
mapBase = 0x0760;
|
||||
waveformBase = 0x07f4;
|
||||
}
|
||||
}
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
detectRevision();
|
||||
ram.allocate(8_KiB, 0x00);
|
||||
harmony.load(cartridge.node);
|
||||
}
|
||||
|
||||
auto unload() -> void override {
|
||||
harmony.unload();
|
||||
}
|
||||
|
||||
auto armInvocation() const -> Harmony::Invocation override {
|
||||
if(revision == Revision::BUS0) return {.pc = 0x0c08, .lr = 0x0c00, .sp = 0x40001ffc};
|
||||
return {.pc = 0x0808, .lr = 0x0800, .sp = 0x40001ffc};
|
||||
}
|
||||
|
||||
auto readARM(u32 mode, n32 address, n32& data) -> Harmony::Access override {
|
||||
auto romBase = mode & ARM7TDMI::Prefetch ? 0x50u : 0x0750u;
|
||||
return readARMMemory(rom, ram, mode, address, romBase, min<u32>(8_KiB, ram.size()), data);
|
||||
}
|
||||
|
||||
auto writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access override {
|
||||
u32 location = address;
|
||||
if(location >= 0x40000000 && location - 0x40000000 < min<u32>(8_KiB, ram.size())) {
|
||||
auto offset = location - 0x40000000;
|
||||
if(offset > 0x28 && offset < 0x06d8) return Harmony::Access::Fault;
|
||||
}
|
||||
return writeARMMemory(ram, mode, address, min<u32>(8_KiB, ram.size()), data);
|
||||
}
|
||||
|
||||
auto trapARM(u32 address, n32& voice, n32 value) -> bool override {
|
||||
constexpr u32 base = 0x06da;
|
||||
if(address < base || address > base + 12 || (address - base) % 4) return false;
|
||||
if(voice >= 3) {
|
||||
if(address == base + 8) voice = 0;
|
||||
return true;
|
||||
}
|
||||
if(address == base + 0) musicFrequency[voice] = value;
|
||||
if(address == base + 4) musicCounter[voice] = 0;
|
||||
if(address == base + 8) voice = musicCounter[voice];
|
||||
if(address == base + 12) waveformShift[voice] = min<u32>(value, 31);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto stepARM(u32 clocks) -> void override {
|
||||
stepAudio(clocks);
|
||||
}
|
||||
|
||||
auto programBase() const -> u32 {
|
||||
return revision == Revision::BUS0 ? 3_KiB : 4_KiB;
|
||||
}
|
||||
|
||||
auto displayBase() const -> u32 {
|
||||
return revision == Revision::BUS0 ? 3_KiB : 2_KiB;
|
||||
}
|
||||
|
||||
auto program(u32 address) const -> n8 {
|
||||
auto offset = programBase() + u32{bank} * 4_KiB + (address & 0x0fff);
|
||||
if(offset < rom.size()) return rom.read(offset);
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto displayRead(u32 address) const -> u8 {
|
||||
if(displayBase() + address < ram.size()) return ram.read(displayBase() + address);
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto displayWrite(u32 address, u8 data) -> void {
|
||||
if(displayBase() + address < ram.size()) ram.write(displayBase() + address, data);
|
||||
}
|
||||
|
||||
auto streamPointer(u8 index) const -> u32 {
|
||||
return read32(datastreamBase + index * 4, ram);
|
||||
}
|
||||
|
||||
auto setStreamPointer(u8 index, u32 value) -> void {
|
||||
write32(datastreamBase + index * 4, value);
|
||||
}
|
||||
|
||||
auto streamIncrement(u8 index) const -> u32 {
|
||||
return read32(incrementBase + index * 4, ram);
|
||||
}
|
||||
|
||||
auto setStreamIncrement(u8 index, u32 value) -> void {
|
||||
write32(incrementBase + index * 4, value);
|
||||
}
|
||||
|
||||
auto addressMap(u8 index) const -> u32 {
|
||||
return read32(mapBase + index * 4, ram);
|
||||
}
|
||||
|
||||
auto setAddressMap(u8 index, u32 value) -> void {
|
||||
write32(mapBase + index * 4, value);
|
||||
}
|
||||
|
||||
auto nextStreamByte(u8 index) -> u8 {
|
||||
auto pointer = streamPointer(index);
|
||||
auto result = displayRead(pointer >> 20);
|
||||
pointer += streamIncrement(index) << 12;
|
||||
setStreamPointer(index, pointer);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto stepAudio(u32 clocks) -> void {
|
||||
audioPhase += clocks;
|
||||
auto ticks = audioPhase / 3500;
|
||||
audioPhase %= 3500;
|
||||
for(u32 voice : range(3)) musicCounter[voice] += u32{musicFrequency[voice]} * ticks;
|
||||
}
|
||||
|
||||
auto waveform(u32 voice) const -> u32 {
|
||||
auto address = read32(waveformBase + voice * 4, ram);
|
||||
if(address < 0x40000800) return 0;
|
||||
address -= 0x40000800;
|
||||
return address < 4_KiB ? address : 0;
|
||||
}
|
||||
|
||||
auto amplitude() -> u8 {
|
||||
if((mode & 0xf0) == 0) {
|
||||
auto address = read32(waveformBase, ram) + (u32{musicCounter[0]} >> 21);
|
||||
u8 sample = 0;
|
||||
if(address < rom.size()) sample = rom.read(address);
|
||||
else if(address >= 0x40000000 && address - 0x40000000 < ram.size()) {
|
||||
sample = ram.read(address - 0x40000000);
|
||||
}
|
||||
if(!(u32{musicCounter[0]} & 1 << 20)) sample >>= 4;
|
||||
return sample & 15;
|
||||
}
|
||||
|
||||
u32 result = 0;
|
||||
for(u32 voice : range(3)) {
|
||||
result += displayRead(waveform(voice) + (u32{musicCounter[voice]} >> waveformShift[voice]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
auto switchBank(u32 address) -> void {
|
||||
if(revision == Revision::BUS0) {
|
||||
if(address >= 0x0ff6 && address <= 0x0ffb) bank = address - 0x0ff6;
|
||||
return;
|
||||
}
|
||||
if(address >= 0x0ff5 && address <= 0x0ffb) bank = address - 0x0ff5;
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
u32 offset = address & 0x0fff;
|
||||
u8 value = program(offset);
|
||||
|
||||
if(revision == Revision::BUS3) {
|
||||
if(fastJumpActive && jumpOperand == offset) {
|
||||
fastJumpActive--;
|
||||
jumpOperand++;
|
||||
auto pointer = streamPointer(JumpStream);
|
||||
auto result = displayRead(pointer >> 20);
|
||||
setStreamPointer(JumpStream, pointer + 0x00100000);
|
||||
return result;
|
||||
}
|
||||
if((mode & 15) == 0 && value == 0x4c && offset <= 0x0ffd
|
||||
&& program(offset + 1) == 0 && program(offset + 2) == 0) {
|
||||
fastJumpActive = 2;
|
||||
jumpOperand = offset + 1;
|
||||
return value;
|
||||
}
|
||||
jumpOperand = 0;
|
||||
}
|
||||
|
||||
if((mode & 15) == 0 && styOperand == offset) busOverdriveAddress = value;
|
||||
styOperand = 0;
|
||||
|
||||
if(revision == Revision::BUS0 && offset < 0x10) return nextStreamByte(offset);
|
||||
if((revision == Revision::BUS1 || revision == Revision::BUS2) && offset < 0x20) {
|
||||
if(offset < 0x10) return nextStreamByte(offset);
|
||||
if(offset == 0x18) return amplitude();
|
||||
return 0;
|
||||
}
|
||||
if(revision == Revision::BUS3 && offset >= 0x0fee && offset <= 0x0ff3) {
|
||||
if(offset == 0x0fee) return amplitude();
|
||||
if(offset == 0x0fef) return nextStreamByte(CommunicationStream);
|
||||
}
|
||||
|
||||
switchBank(offset);
|
||||
if((mode & 15) == 0 && value == 0x84) styOperand = offset + 1;
|
||||
return value;
|
||||
}
|
||||
|
||||
auto writeBelowCartridge(n16 address, n8 data) -> n8 {
|
||||
bool matches = address == busOverdriveAddress;
|
||||
busOverdriveAddress = 0xff;
|
||||
if(!matches) return data;
|
||||
auto target = u8(address & 0x7f);
|
||||
if(target > 0x24) return data;
|
||||
auto mappings = addressMap(target);
|
||||
auto stream = u8(mappings & 15);
|
||||
auto stuffed = u8(data) & nextStreamByte(stream);
|
||||
setAddressMap(target, mappings >> 4 | u32{stream} << 28);
|
||||
return stuffed;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return writeBelowCartridge(address, data);
|
||||
u32 offset = address & 0x0fff;
|
||||
|
||||
if(revision == Revision::BUS0) {
|
||||
auto index = u8(offset & 15);
|
||||
if(offset >= 0x10 && offset <= 0x13) {
|
||||
auto pointer = streamPointer(index);
|
||||
displayWrite(pointer >> 20, data);
|
||||
setStreamPointer(index, pointer + 0x00100000);
|
||||
}
|
||||
if(offset == 0x1b && (data == 254 || data == 255)) harmony.call(data == 254);
|
||||
if(offset == 0x1c) mode = data == 0 ? 0 : 0x0f;
|
||||
if(offset >= 0x20 && offset <= 0x2f) {
|
||||
auto pointer = streamPointer(index) << 8;
|
||||
setStreamPointer(index, pointer & 0xf0000000 | u32{data} << 20);
|
||||
}
|
||||
if(offset >= 0x30 && offset <= 0x3f) setStreamIncrement(index, data);
|
||||
switchBank(offset);
|
||||
return data;
|
||||
}
|
||||
|
||||
switchBank(offset);
|
||||
if(revision == Revision::BUS3) {
|
||||
if(offset == 0x0ff0) {
|
||||
auto pointer = streamPointer(CommunicationStream);
|
||||
displayWrite(pointer >> 20, data);
|
||||
setStreamPointer(CommunicationStream, pointer + 0x00100000);
|
||||
}
|
||||
if(offset == 0x0ff1) {
|
||||
auto pointer = streamPointer(CommunicationStream) << 8;
|
||||
setStreamPointer(CommunicationStream, pointer & 0xf0000000 | u32{data} << 20);
|
||||
}
|
||||
if(offset == 0x0ff2) mode = data;
|
||||
if(offset == 0x0ff3 && (data == 254 || data == 255)) harmony.call(data == 254);
|
||||
return data;
|
||||
}
|
||||
|
||||
if(offset >= 0x10 && offset <= 0x1f) {
|
||||
auto index = u8(offset & 15);
|
||||
if(index <= 3) {
|
||||
auto pointer = streamPointer(index);
|
||||
displayWrite(pointer >> 20, data);
|
||||
setStreamPointer(index, pointer + 0x00100000);
|
||||
}
|
||||
if(index >= 4 && index <= 7) {
|
||||
index &= 3;
|
||||
auto pointer = streamPointer(index) << 8;
|
||||
setStreamPointer(index, pointer & 0xf0000000 | u32{data} << 20);
|
||||
}
|
||||
if(index == 9) mode = data == 0 ? 0 : 0x0f;
|
||||
if(index == 10 && (data == 254 || data == 255)) harmony.call(data == 254);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
ram.fill(0x00);
|
||||
auto driverSize = revision == Revision::BUS0 ? 3_KiB : 2_KiB;
|
||||
for(u32 offset : range(min<u32>(driverSize, rom.size()))) ram.write(offset, rom.read(offset));
|
||||
bank = revision == Revision::BUS0 ? 5 : 6;
|
||||
mode = 0xff;
|
||||
fastJumpActive = 0;
|
||||
styOperand = 0;
|
||||
busOverdriveAddress = 0;
|
||||
jumpOperand = 0;
|
||||
for(auto& item : musicCounter) item = 0;
|
||||
for(auto& item : musicFrequency) item = 0;
|
||||
for(auto& item : waveformShift) item = 27;
|
||||
audioPhase = 0;
|
||||
harmony.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(ram);
|
||||
s(bank);
|
||||
s(mode);
|
||||
s(fastJumpActive);
|
||||
s(styOperand);
|
||||
s(busOverdriveAddress);
|
||||
s(jumpOperand);
|
||||
for(auto& item : musicCounter) s(item);
|
||||
for(auto& item : musicFrequency) s(item);
|
||||
for(auto& item : waveformShift) s(item);
|
||||
s(audioPhase);
|
||||
harmony.serialize(s);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
struct CbsRamPlus : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
n2 bank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
if(address >= 0x1ff8 && address <= 0x1ffa) bank = address - 0x1ff8;
|
||||
if(address >= 0x1000 && address <= 0x10ff) {
|
||||
ram.write(address & 0xff, data);
|
||||
return data;
|
||||
}
|
||||
if(address >= 0x1100 && address <= 0x11ff) return ram.read(address & 0xff);
|
||||
if(address.bit(12)) return rom.read((bank * 0x1000) + (address & 0x0fff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
if(address >= 0x1ff8 && address <= 0x1ffa) bank = address - 0x1ff8;
|
||||
if(address >= 0x1000 && address <= 0x10ff) {
|
||||
ram.write(address & 0xff, data);
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
bank = 2;
|
||||
ram.allocate(256, 0x00);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
s(ram);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,354 @@
|
||||
struct CDF : Interface {
|
||||
using Interface::Interface;
|
||||
|
||||
enum class Revision : u32 { CDF0, CDF1, CDFJ, CDFJPlus };
|
||||
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
Revision revision = Revision::CDF0;
|
||||
n3 bank;
|
||||
n8 mode;
|
||||
n2 fastJumpActive;
|
||||
n6 fastJumpStream;
|
||||
n16 immediateOperand;
|
||||
n16 jumpOperand;
|
||||
n32 musicCounter[3];
|
||||
n32 musicFrequency[3];
|
||||
n8 waveformShift[3];
|
||||
u16 datastreamBase = 0;
|
||||
u16 incrementBase = 0;
|
||||
u16 waveformBase = 0;
|
||||
u16 fastFetcherOffset = 0;
|
||||
u8 amplitudeStream = 0;
|
||||
u8 fastJumpMask = 0;
|
||||
bool ldxEnabled = false;
|
||||
bool ldyEnabled = false;
|
||||
u32 audioPhase = 0;
|
||||
|
||||
static constexpr u8 CommunicationStream = 0x20;
|
||||
static constexpr u8 JumpStreamBase = 0x21;
|
||||
static constexpr u16 NoImmediate = 0xffff;
|
||||
|
||||
template<typename MemoryType>
|
||||
auto read32(u32 address, const MemoryType& memory) const -> u32 {
|
||||
if(address + 3 >= memory.size()) return 0;
|
||||
return memory.read(address + 0) << 0 | memory.read(address + 1) << 8
|
||||
| memory.read(address + 2) << 16 | memory.read(address + 3) << 24;
|
||||
}
|
||||
|
||||
auto write32(u32 address, u32 data) -> void {
|
||||
if(address + 3 >= ram.size()) return;
|
||||
for(u32 byte : range(4)) ram.write(address + byte, data >> byte * 8);
|
||||
}
|
||||
|
||||
auto scan(u32 value) const -> u32 {
|
||||
if(rom.size() < 2_KiB) return ~0u;
|
||||
for(u32 offset = 0; offset < 2_KiB; offset += 4) {
|
||||
if(read32(offset, rom) == value) return offset;
|
||||
}
|
||||
return ~0u;
|
||||
}
|
||||
|
||||
auto detectRevision() -> void {
|
||||
auto plusSignature = scan(0x53554c50);
|
||||
if(plusSignature != ~0u && read32(plusSignature + 4, rom) == 0x4a464443
|
||||
&& read32(plusSignature + 8, rom) == 1) {
|
||||
revision = Revision::CDFJPlus;
|
||||
amplitudeStream = 0x23;
|
||||
fastJumpMask = 0xfe;
|
||||
datastreamBase = 0x0098;
|
||||
incrementBase = 0x0124;
|
||||
waveformBase = 0x01b0;
|
||||
for(u32 offset = 0; offset < 2_KiB; offset += 4) {
|
||||
auto word = read32(offset, rom);
|
||||
if(word == 0x135200a2) ldxEnabled = true;
|
||||
if(word == 0x135200a0) ldyEnabled = true;
|
||||
if((word & 0xffffff00) == 0xe2422000) fastFetcherOffset = offset;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
u8 subversion = 0;
|
||||
for(u32 offset = 0; offset + 11 < min<u32>(rom.size(), 2_KiB); offset += 4) {
|
||||
if(rom.read(offset + 0) == 'C' && rom.read(offset + 1) == 'D' && rom.read(offset + 2) == 'F'
|
||||
&& rom.read(offset + 4) == 'C' && rom.read(offset + 5) == 'D' && rom.read(offset + 6) == 'F'
|
||||
&& rom.read(offset + 8) == 'C' && rom.read(offset + 9) == 'D' && rom.read(offset + 10) == 'F') {
|
||||
subversion = rom.read(offset + 3);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(subversion == 0x4a) {
|
||||
revision = Revision::CDFJ;
|
||||
amplitudeStream = 0x23;
|
||||
fastJumpMask = 0xfe;
|
||||
datastreamBase = 0x0098;
|
||||
incrementBase = 0x0124;
|
||||
waveformBase = 0x01b0;
|
||||
} else if(subversion == 0) {
|
||||
revision = Revision::CDF0;
|
||||
amplitudeStream = 0x22;
|
||||
fastJumpMask = 0xff;
|
||||
datastreamBase = 0x06e0;
|
||||
incrementBase = 0x0768;
|
||||
waveformBase = 0x07f0;
|
||||
} else {
|
||||
revision = Revision::CDF1;
|
||||
amplitudeStream = 0x22;
|
||||
fastJumpMask = 0xff;
|
||||
datastreamBase = 0x00a0;
|
||||
incrementBase = 0x0128;
|
||||
waveformBase = 0x01b0;
|
||||
}
|
||||
}
|
||||
|
||||
auto isPlus() const -> bool {
|
||||
return revision == Revision::CDFJPlus;
|
||||
}
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
detectRevision();
|
||||
ram.allocate(isPlus() ? 32_KiB : 8_KiB, 0x00);
|
||||
harmony.load(cartridge.node);
|
||||
}
|
||||
|
||||
auto unload() -> void override {
|
||||
harmony.unload();
|
||||
}
|
||||
|
||||
auto armInvocation() const -> Harmony::Invocation override {
|
||||
if(isPlus()) {
|
||||
auto address = read32(0x17f8, rom) & ~1u;
|
||||
return {.pc = address, .lr = address, .sp = read32(0x17f4, rom)};
|
||||
}
|
||||
return {.pc = 0x0808, .lr = 0x0800, .sp = 0x40001ffc};
|
||||
}
|
||||
|
||||
auto armRamLimit() const -> u32 {
|
||||
if(!isPlus()) return min<u32>(8_KiB, ram.size());
|
||||
if(rom.size() == 64_KiB || rom.size() == 128_KiB) return min<u32>(16_KiB, ram.size());
|
||||
if(rom.size() == 256_KiB || rom.size() == 512_KiB) return min<u32>(32_KiB, ram.size());
|
||||
return min<u32>(8_KiB, ram.size());
|
||||
}
|
||||
|
||||
auto readARM(u32 mode, n32 address, n32& data) -> Harmony::Access override {
|
||||
auto romBase = mode & ARM7TDMI::Prefetch ? 0x50u : 0x0750u;
|
||||
return readARMMemory(rom, ram, mode, address, romBase, armRamLimit(), data);
|
||||
}
|
||||
|
||||
auto writeProtectedARM(u32 offset) const -> bool {
|
||||
if(revision == Revision::CDF0) {
|
||||
return offset > 0x28 && offset < 0x0800 && !(offset >= 0x06e0 && offset < 0x07fc);
|
||||
}
|
||||
if(revision == Revision::CDF1) {
|
||||
return offset > 0x28 && offset < 0x0800 && !(offset >= 0x00a0 && offset < 0x01bc);
|
||||
}
|
||||
return offset > 0x28 && offset < 0x0800 && !(offset >= 0x0098 && offset < 0x01bc)
|
||||
&& !(isPlus() && offset == 0x03e0);
|
||||
}
|
||||
|
||||
auto writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access override {
|
||||
u32 location = address;
|
||||
if(location >= 0x40000000 && location - 0x40000000 < armRamLimit()) {
|
||||
if(writeProtectedARM(location - 0x40000000)) return Harmony::Access::Fault;
|
||||
}
|
||||
return writeARMMemory(ram, mode, address, armRamLimit(), data);
|
||||
}
|
||||
|
||||
auto trapARM(u32 address, n32& voice, n32 value) -> bool override {
|
||||
u32 base = revision == Revision::CDF0 ? 0x06e2 : 0x0752;
|
||||
if(address < base || address > base + 12 || (address - base) % 4) return false;
|
||||
if(voice >= 3) {
|
||||
if(address == base + 8) voice = 0;
|
||||
return true;
|
||||
}
|
||||
if(address == base + 0) musicFrequency[voice] = value;
|
||||
if(address == base + 4) musicCounter[voice] = 0;
|
||||
if(address == base + 8) voice = musicCounter[voice];
|
||||
if(address == base + 12) waveformShift[voice] = min<u32>(value, 31);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto stepARM(u32 clocks) -> void override {
|
||||
stepAudio(clocks);
|
||||
}
|
||||
|
||||
auto programBase() const -> u32 {
|
||||
return isPlus() ? 2_KiB : 4_KiB;
|
||||
}
|
||||
|
||||
auto program(u32 address) const -> n8 {
|
||||
u32 offset = programBase() + u32{bank} * 4_KiB + (address & 0x0fff);
|
||||
if(offset < rom.size()) return rom.read(offset);
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto displaySize() const -> u32 {
|
||||
return ram.size() > 2_KiB ? ram.size() - 2_KiB : 0;
|
||||
}
|
||||
|
||||
auto displayRead(u32 address) const -> u8 {
|
||||
if(address < displaySize()) return ram.read(2_KiB + address);
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto displayWrite(u32 address, u8 data) -> void {
|
||||
if(address < displaySize()) ram.write(2_KiB + address, data);
|
||||
}
|
||||
|
||||
auto streamPointer(u8 index) const -> u32 {
|
||||
return read32(datastreamBase + index * 4, ram);
|
||||
}
|
||||
|
||||
auto setStreamPointer(u8 index, u32 value) -> void {
|
||||
write32(datastreamBase + index * 4, value);
|
||||
}
|
||||
|
||||
auto streamIncrement(u8 index) const -> u32 {
|
||||
return read32(incrementBase + index * 4, ram);
|
||||
}
|
||||
|
||||
auto nextStreamByte(u8 index) -> u8 {
|
||||
if(index > amplitudeStream) return 0;
|
||||
auto pointer = streamPointer(index);
|
||||
u32 location = isPlus() ? pointer >> 16 : pointer >> 20;
|
||||
auto data = displayRead(location);
|
||||
pointer += streamIncrement(index) << (isPlus() ? 8 : 12);
|
||||
setStreamPointer(index, pointer);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto waveform(u32 voice) const -> u32 {
|
||||
u32 address = read32(waveformBase + voice * 4, ram);
|
||||
address -= 0x40000800;
|
||||
if(!isPlus() && address >= 4_KiB) address &= 0x0fff;
|
||||
if(isPlus() && address >= displaySize()) return 0;
|
||||
return address;
|
||||
}
|
||||
|
||||
auto stepAudio(u32 clocks) -> void {
|
||||
audioPhase += clocks;
|
||||
auto ticks = audioPhase / 3500;
|
||||
audioPhase %= 3500;
|
||||
for(u32 voice : range(3)) musicCounter[voice] += u32{musicFrequency[voice]} * ticks;
|
||||
}
|
||||
|
||||
auto audioValue() -> u8 {
|
||||
if((mode & 0xf0) == 0) {
|
||||
u32 address = read32(waveformBase, ram) + (u32{musicCounter[0]} >> (isPlus() ? 13 : 21));
|
||||
u8 sample = 0;
|
||||
if(address < rom.size()) sample = rom.read(address);
|
||||
else if(address >= 0x40000000 && address - 0x40000000 < ram.size()) sample = ram.read(address - 0x40000000);
|
||||
if(!(u32{musicCounter[0]} & 1 << (isPlus() ? 12 : 20))) sample >>= 4;
|
||||
return sample & 15;
|
||||
}
|
||||
|
||||
u32 result = 0;
|
||||
for(u32 voice : range(3)) {
|
||||
auto location = waveform(voice) + (u32{musicCounter[voice]} >> waveformShift[voice]);
|
||||
result += displayRead(location);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
auto switchBank(u32 address) -> void {
|
||||
if(address < 0x0ff4 || address > 0x0ffb) return;
|
||||
if(isPlus()) bank = address == 0x0ffb ? 0 : address - 0x0ff4;
|
||||
else bank = address == 0x0ff4 || address == 0x0ffb ? 6 : address - 0x0ff5;
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
u32 offset = address & 0x0fff;
|
||||
u8 value = program(offset);
|
||||
|
||||
if(fastJumpActive && jumpOperand == offset) {
|
||||
fastJumpActive--;
|
||||
jumpOperand++;
|
||||
auto pointer = streamPointer(fastJumpStream);
|
||||
auto result = displayRead(isPlus() ? pointer >> 16 : pointer >> 20);
|
||||
pointer += isPlus() ? 0x00010000 : 0x00100000;
|
||||
setStreamPointer(fastJumpStream, pointer);
|
||||
return result;
|
||||
}
|
||||
|
||||
if((mode & 15) == 0 && value == 0x4c && offset <= 0x0ffd
|
||||
&& !(program(offset + 1) & fastJumpMask) && program(offset + 2) == 0) {
|
||||
fastJumpActive = 2;
|
||||
jumpOperand = offset + 1;
|
||||
fastJumpStream = program(offset + 1) + JumpStreamBase;
|
||||
return value;
|
||||
}
|
||||
jumpOperand = 0;
|
||||
|
||||
bool fastFetch = false;
|
||||
if((mode & 15) == 0 && immediateOperand == offset) {
|
||||
u8 base = 0;
|
||||
if(fastFetcherOffset && fastFetcherOffset < ram.size()) base = ram.read(fastFetcherOffset);
|
||||
fastFetch = value >= base && u32{value} <= u32{base} + amplitudeStream;
|
||||
if(fastFetch && fastFetcherOffset) value -= base;
|
||||
}
|
||||
immediateOperand = NoImmediate;
|
||||
if(fastFetch) return value == amplitudeStream ? audioValue() : nextStreamByte(value);
|
||||
|
||||
switchBank(offset);
|
||||
if((mode & 15) == 0 && (value == 0xa9 || (ldxEnabled && value == 0xa2) || (ldyEnabled && value == 0xa0))) {
|
||||
immediateOperand = offset + 1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
u32 offset = address & 0x0fff;
|
||||
if(offset == 0x0ff0) {
|
||||
auto pointer = streamPointer(CommunicationStream);
|
||||
displayWrite(isPlus() ? pointer >> 16 : pointer >> 20, data);
|
||||
setStreamPointer(CommunicationStream, pointer + (isPlus() ? 0x00010000 : 0x00100000));
|
||||
}
|
||||
if(offset == 0x0ff1) {
|
||||
auto pointer = streamPointer(CommunicationStream) << 8;
|
||||
pointer = isPlus() ? (pointer & 0xff000000) | u32{data} << 16
|
||||
: (pointer & 0xf0000000) | u32{data} << 20;
|
||||
setStreamPointer(CommunicationStream, pointer);
|
||||
}
|
||||
if(offset == 0x0ff2) mode = data;
|
||||
if(offset == 0x0ff3 && (data == 254 || data == 255)) harmony.call(data == 254);
|
||||
switchBank(offset);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
ram.fill(0x00);
|
||||
for(u32 offset : range(min<u32>(2_KiB, rom.size()))) ram.write(offset, rom.read(offset));
|
||||
bank = isPlus() ? 0 : 6;
|
||||
mode = 0xff;
|
||||
fastJumpActive = 0;
|
||||
fastJumpStream = 0;
|
||||
immediateOperand = NoImmediate;
|
||||
jumpOperand = 0;
|
||||
for(auto& item : musicCounter) item = 0;
|
||||
for(auto& item : musicFrequency) item = 0;
|
||||
for(auto& item : waveformShift) item = 27;
|
||||
audioPhase = 0;
|
||||
harmony.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(ram);
|
||||
s(bank);
|
||||
s(mode);
|
||||
s(fastJumpActive);
|
||||
s(fastJumpStream);
|
||||
s(immediateOperand);
|
||||
s(jumpOperand);
|
||||
for(auto& item : musicCounter) s(item);
|
||||
for(auto& item : musicFrequency) s(item);
|
||||
for(auto& item : waveformShift) s(item);
|
||||
s(audioPhase);
|
||||
harmony.serialize(s);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
struct Chetiry : Interface, Thread {
|
||||
using Interface::Interface;
|
||||
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Readable<n8> tunes;
|
||||
Memory::Writable<n8> ram;
|
||||
PersistentMemory persistent;
|
||||
n3 bank;
|
||||
n8 operation;
|
||||
n16 tunePosition;
|
||||
n1 ldaImmediate;
|
||||
n32 random = 0x2b435044;
|
||||
n32 musicCounter[3];
|
||||
n32 musicFrequency[3];
|
||||
n15 busyTicks;
|
||||
n1 operationCompleted;
|
||||
n1 operationSucceeded;
|
||||
n3 tune;
|
||||
|
||||
auto load() -> void override {
|
||||
Memory::Readable<n8> image;
|
||||
Interface::load(image, "program.rom");
|
||||
rom.allocate(32_KiB, 0x00);
|
||||
for(u32 offset : range(min<u32>(image.size(), 32_KiB))) rom.program(offset, image.read(offset));
|
||||
tunes.allocate(28_KiB, 0x00);
|
||||
if(image.size() > 32_KiB) {
|
||||
for(u32 offset : range(min<u32>(image.size() - 32_KiB, 28_KiB))) {
|
||||
tunes.program(offset, image.read(32_KiB + offset));
|
||||
}
|
||||
}
|
||||
persistent.load(pak, "save.eeprom", 256, 0x00);
|
||||
}
|
||||
|
||||
auto save() -> void override {
|
||||
persistent.flush(pak);
|
||||
}
|
||||
|
||||
auto unload() -> void override {
|
||||
persistent.flush(pak);
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
auto value = rom.read(bank * 4_KiB + (address & 0x0fff));
|
||||
if(ldaImmediate && value == 0xf2) {
|
||||
ldaImmediate = 0;
|
||||
return (musicCounter[0] >> 31) + (musicCounter[1] >> 31) + (musicCounter[2] >> 31) << 2;
|
||||
}
|
||||
ldaImmediate = 0;
|
||||
|
||||
if(address <= 0x103f) {
|
||||
ram.write(address & 0x3f, data);
|
||||
return data;
|
||||
}
|
||||
if(address <= 0x107f) return readRAM(address & 0x3f);
|
||||
if(address == 0x1ff4) return operate(value);
|
||||
bankswitch(address);
|
||||
ldaImmediate = value == 0xa9;
|
||||
return value;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
if(address <= 0x103f) {
|
||||
writeRAM(address & 0x3f, data);
|
||||
return data;
|
||||
}
|
||||
if(address == 0x1ff4) {
|
||||
operate(rom.read(bank * 4_KiB + 0x0ff4));
|
||||
return data;
|
||||
}
|
||||
bankswitch(address);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
for(u32 channel : range(3)) musicCounter[channel] += musicFrequency[channel];
|
||||
if(busyTicks && !--busyTicks) {
|
||||
operationCompleted = 1;
|
||||
ram.write(0, operationSucceeded ? 0x00 : 0xff);
|
||||
}
|
||||
step(1);
|
||||
Thread::synchronize(cpu);
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
Thread::create(20'000, std::bind_front(&Chetiry::main, this));
|
||||
bank = 1;
|
||||
operation = 0;
|
||||
tunePosition = 0;
|
||||
ldaImmediate = 0;
|
||||
random = 0x2b435044;
|
||||
busyTicks = 0;
|
||||
operationCompleted = 0;
|
||||
operationSucceeded = 0;
|
||||
tune = 0;
|
||||
for(auto& counter : musicCounter) counter = 0;
|
||||
for(auto& frequency : musicFrequency) frequency = 0;
|
||||
ram.allocate(64, 0x00);
|
||||
for(u32 offset : range(4)) ram.write(offset, 0xff);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
Thread::serialize(s);
|
||||
s(bank);
|
||||
s(operation);
|
||||
s(tunePosition);
|
||||
s(ldaImmediate);
|
||||
s(random);
|
||||
s(musicCounter);
|
||||
s(musicFrequency);
|
||||
s(busyTicks);
|
||||
s(operationCompleted);
|
||||
s(operationSucceeded);
|
||||
s(tune);
|
||||
s(ram);
|
||||
}
|
||||
|
||||
private:
|
||||
static auto noteFrequency(u32 note) -> u32 {
|
||||
static constexpr u32 table[63] = {
|
||||
0, 0, 0, 11811160, 12513387, 13257490, 14045832, 14881203, 15765966,
|
||||
16703557, 17696768, 18749035, 19864009, 21045125, 22296464, 23622320,
|
||||
25026989, 26515195, 28091878, 29762191, 31531932, 33406900, 35393537,
|
||||
37498071, 39727803, 42090250, 44592927, 47244640, 50053978, 53030391,
|
||||
56183756, 59524596, 63064079, 66814014, 70787074, 74996142, 79455606,
|
||||
84180285, 89186069, 94489281, 100107957, 106060567, 112367297,
|
||||
119048977, 126128157, 133628029, 141573933, 149992288, 158911428,
|
||||
168360785, 178371925, 188978561, 200215913, 212121348, 224734593,
|
||||
238098169, 252256099, 267256058, 283147866, 299984783, 317822855,
|
||||
336721571, 356744064,
|
||||
};
|
||||
return note < 63 ? table[note] : 0;
|
||||
}
|
||||
|
||||
auto bankswitch(n16 address) -> void {
|
||||
if(address >= 0x1ff5 && address <= 0x1ffb) bank = address - 0x1ff4;
|
||||
}
|
||||
|
||||
auto readRAM(u32 address) -> n8 {
|
||||
if(address == 0) return ram.read(0);
|
||||
if(address == 1) {
|
||||
random = (random.bit(10) ? 0x10adab1e : 0) ^ (u32{random} >> 11 | u32{random} << 21);
|
||||
return random.byte(0);
|
||||
}
|
||||
if(address == 2) return tunePosition.byte(0);
|
||||
if(address == 3) return tunePosition.byte(1);
|
||||
return ram.read(address);
|
||||
}
|
||||
|
||||
auto writeRAM(u32 address, n8 data) -> void {
|
||||
if(address == 0) operation = data;
|
||||
else if(address == 1) random = 0x2b435044;
|
||||
else if(address == 2) {
|
||||
tunePosition = 0;
|
||||
for(auto& counter : musicCounter) counter = 0;
|
||||
for(auto& frequency : musicFrequency) frequency = 0;
|
||||
} else if(address == 3) {
|
||||
updateTune();
|
||||
} else {
|
||||
ram.write(address, data);
|
||||
}
|
||||
}
|
||||
|
||||
auto updateTune() -> void {
|
||||
auto position = u32{tune} * 4_KiB + u32{tunePosition} * 3;
|
||||
if(position + 2 >= tunes.size() || position + 2 >= (u32{tune} + 1) * 4_KiB) {
|
||||
tunePosition = 0;
|
||||
return;
|
||||
}
|
||||
tunePosition++;
|
||||
auto note0 = tunes.read(position + 0);
|
||||
auto note1 = tunes.read(position + 1);
|
||||
auto note2 = tunes.read(position + 2);
|
||||
if(note0) musicFrequency[0] = noteFrequency(note0);
|
||||
if(note1) musicFrequency[1] = noteFrequency(note1);
|
||||
if(note2 == 1) tunePosition = 0;
|
||||
else musicFrequency[2] = noteFrequency(note2);
|
||||
}
|
||||
|
||||
auto operate(n8 value) -> n8 {
|
||||
if(busyTicks) return value | 0x40;
|
||||
if(operationCompleted) {
|
||||
operationCompleted = 0;
|
||||
return value & ~0x40;
|
||||
}
|
||||
auto index = u32{operation} >> 4;
|
||||
auto command = u32{operation} & 15;
|
||||
operationSucceeded = 1;
|
||||
if(command == 1 && index < 7) {
|
||||
tune = index;
|
||||
tunePosition = 0;
|
||||
busyTicks = 10'000;
|
||||
} else if(command == 2 && index < 4) {
|
||||
for(u32 offset : range(4, 64)) ram.write(offset, persistent.read(index * 64 + offset));
|
||||
busyTicks = 10'000;
|
||||
} else if(command == 3 && index < 4) {
|
||||
for(u32 offset : range(4, 64)) persistent.write(index * 64 + offset, ram.read(offset));
|
||||
busyTicks = 20'000;
|
||||
} else if(command == 4) {
|
||||
persistent.erase();
|
||||
busyTicks = 20'000;
|
||||
} else {
|
||||
operationSucceeded = 0;
|
||||
ram.write(0, 0xff);
|
||||
return value & ~0x40;
|
||||
}
|
||||
return value | 0x40;
|
||||
}
|
||||
};
|
||||
@@ -13,25 +13,25 @@ struct Commavid : Interface {
|
||||
auto unload() -> void override {
|
||||
}
|
||||
|
||||
auto read(n16 address) -> n8 override {
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
if(address.bit(12)) {
|
||||
if(address >= 0x1000 && address <= 0x13ff) return ram.read(address & 0x3ff);
|
||||
return rom.read(address & 0xfff);
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> bool override {
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
if(address >= 0x1400 && address <= 0x17ff) {
|
||||
ram.write(address & 0x3ff, data);
|
||||
return true;
|
||||
return data;
|
||||
}
|
||||
|
||||
return false;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power() -> void override {
|
||||
auto power(bool reset) -> void override {
|
||||
ram.allocate(1_KiB);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
struct CPUWiz4KSC : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto save() -> void override {
|
||||
}
|
||||
|
||||
auto unload() -> void override {
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(address < 0x1000) return data;
|
||||
if(address <= 0x107f) {
|
||||
ram.write(address & 0x7f, data);
|
||||
return data;
|
||||
}
|
||||
if(address <= 0x10ff) return ram.read(address & 0x7f);
|
||||
return rom.read(address & 0xfff);
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(address < 0x1000 || address > 0x107f) return data;
|
||||
ram.write(address & 0x7f, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
ram.allocate(128, 0x00);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(ram);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,276 @@
|
||||
struct DPCPlus : Interface {
|
||||
using Interface::Interface;
|
||||
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
n3 bank;
|
||||
n8 top[8];
|
||||
n8 bottom[8];
|
||||
n12 counter[8];
|
||||
n20 fractionalCounter[8];
|
||||
n8 fractionalIncrement[8];
|
||||
n32 musicCounter[3];
|
||||
n32 musicFrequency[3];
|
||||
n7 musicWaveform[3];
|
||||
n32 random;
|
||||
n1 fastFetch;
|
||||
n1 ldaImmediate;
|
||||
n8 parameter[8];
|
||||
n4 parameterPointer;
|
||||
u32 fractionalLowMask = 0x0f00ff;
|
||||
u32 audioPhase = 0;
|
||||
|
||||
auto load() -> void override {
|
||||
Memory::Readable<n8> image;
|
||||
Interface::load(image, "program.rom");
|
||||
auto size = min<u32>(image.size(), 32_KiB);
|
||||
rom.allocate(32_KiB, 0x00);
|
||||
auto destination = 32_KiB - size;
|
||||
for(u32 offset : range(size)) rom.program(destination + offset, image.read(offset));
|
||||
|
||||
if(image.size() >= 3_KiB) {
|
||||
Hash::SHA256 hash;
|
||||
for(u32 offset : range(3_KiB)) hash.input((u8)image.read(offset));
|
||||
auto driver = hash.digest();
|
||||
if(driver == "bcbe33dc02ad5104f3a3a53a856a23c57f435344790843f4fb6e746193468d0c"
|
||||
|| driver == "445ca7866a0efdf0e35076a25e963708dc630f80fd82dcdaf775fb80c8b0c58d") {
|
||||
fractionalLowMask = 0x0f0000;
|
||||
}
|
||||
}
|
||||
ram.allocate(8_KiB, 0x00);
|
||||
harmony.load(cartridge.node);
|
||||
}
|
||||
|
||||
auto unload() -> void override {
|
||||
harmony.unload();
|
||||
}
|
||||
|
||||
auto armInvocation() const -> Harmony::Invocation override {
|
||||
return {.pc = 0x0c08, .lr = 0x0c00, .sp = 0x40001ffc};
|
||||
}
|
||||
|
||||
auto readARM(u32 mode, n32 address, n32& data) -> Harmony::Access override {
|
||||
auto romBase = mode & ARM7TDMI::Prefetch ? 0x50u : 0x0c00u;
|
||||
return readARMMemory(rom, ram, mode, address, romBase, min<u32>(8_KiB, ram.size()), data);
|
||||
}
|
||||
|
||||
auto writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access override {
|
||||
u32 location = address;
|
||||
if(location >= 0x40000000 && location - 0x40000000 < min<u32>(8_KiB, ram.size())) {
|
||||
auto offset = location - 0x40000000;
|
||||
if(offset > 0x28 && offset < 0x0c00) return Harmony::Access::Fault;
|
||||
}
|
||||
return writeARMMemory(ram, mode, address, min<u32>(8_KiB, ram.size()), data);
|
||||
}
|
||||
|
||||
auto stepARM(u32 clocks) -> void override {
|
||||
stepAudio(clocks);
|
||||
}
|
||||
|
||||
auto program(u32 address) const -> n8 {
|
||||
return rom.read(3_KiB + bank * 4_KiB + (address & 0x0fff));
|
||||
}
|
||||
|
||||
auto displayRead(u32 address) const -> n8 {
|
||||
return ram.read(3_KiB + (address & 0x0fff));
|
||||
}
|
||||
|
||||
auto displayWrite(u32 address, n8 data) -> void {
|
||||
ram.write(3_KiB + (address & 0x0fff), data);
|
||||
}
|
||||
|
||||
auto frequency(u32 note) const -> u32 {
|
||||
u32 address = 7_KiB + (note << 2);
|
||||
return ram.read(address + 0) << 0 | ram.read(address + 1) << 8
|
||||
| ram.read(address + 2) << 16 | ram.read(address + 3) << 24;
|
||||
}
|
||||
|
||||
auto clockRandom() -> void {
|
||||
random = (random.bit(10) ? 0x10adab1e : 0) ^ (u32{random} >> 11 | u32{random} << 21);
|
||||
}
|
||||
|
||||
auto priorRandom() -> void {
|
||||
u32 value = random;
|
||||
if(value >> 31) value ^= 0x10adab1e;
|
||||
random = value << 11 | value >> 21;
|
||||
}
|
||||
|
||||
auto stepAudio(u32 clocks) -> void {
|
||||
audioPhase += clocks;
|
||||
auto ticks = audioPhase / 3500;
|
||||
audioPhase %= 3500;
|
||||
for(u32 channel : range(3)) musicCounter[channel] += u32{musicFrequency[channel]} * ticks;
|
||||
}
|
||||
|
||||
auto amplitude() -> n8 {
|
||||
u32 value = 0;
|
||||
for(u32 channel : range(3)) {
|
||||
value += displayRead((u32{musicWaveform[channel]} << 5) + (u32{musicCounter[channel]} >> 27));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
auto readRegister(u32 address) -> n8 {
|
||||
u32 index = address & 7;
|
||||
u32 function = address >> 3 & 7;
|
||||
u8 topDistance = u8(u32{top[index]} - (u32{counter[index]} & 0xff));
|
||||
u8 window = u8(u32{top[index]} - u32{bottom[index]});
|
||||
n8 flag = topDistance > window ? 0xff : 0x00;
|
||||
|
||||
if(function == 0) {
|
||||
if(index == 0) return clockRandom(), random.byte(0);
|
||||
if(index == 1) return priorRandom(), random.byte(0);
|
||||
if(index == 2) return random.byte(1);
|
||||
if(index == 3) return random.byte(2);
|
||||
if(index == 4) return random.byte(3);
|
||||
if(index == 5) return amplitude();
|
||||
return 0;
|
||||
}
|
||||
if(function == 1) {
|
||||
auto data = displayRead(counter[index]);
|
||||
counter[index]++;
|
||||
return data;
|
||||
}
|
||||
if(function == 2) {
|
||||
auto data = displayRead(counter[index]) & flag;
|
||||
counter[index]++;
|
||||
return data;
|
||||
}
|
||||
if(function == 3) {
|
||||
auto data = displayRead(u32{fractionalCounter[index]} >> 8);
|
||||
fractionalCounter[index] += fractionalIncrement[index];
|
||||
return data;
|
||||
}
|
||||
if(function == 4 && index < 4) return flag;
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto callFunction(n8 function) -> void {
|
||||
u32 source = parameter[0] | parameter[1] << 8;
|
||||
if(function == 0) parameterPointer = 0;
|
||||
if(function == 1 || function == 2) {
|
||||
u32 destination = counter[parameter[2] & 7];
|
||||
u32 count = parameter[3];
|
||||
if(destination < 4_KiB) {
|
||||
count = min(count, 4_KiB - destination);
|
||||
if(function == 1 && source < rom.size() - 3_KiB) {
|
||||
count = min(count, rom.size() - 3_KiB - source);
|
||||
for(u32 offset : range(count)) displayWrite(destination + offset, rom.read(3_KiB + source + offset));
|
||||
}
|
||||
if(function == 2) {
|
||||
for(u32 offset : range(count)) displayWrite(destination + offset, parameter[0]);
|
||||
}
|
||||
}
|
||||
parameterPointer = 0;
|
||||
}
|
||||
if(function == 254 || function == 255) {
|
||||
harmony.call(function == 254);
|
||||
}
|
||||
}
|
||||
|
||||
auto writeRegister(u32 address, n8 data) -> void {
|
||||
u32 index = address & 7;
|
||||
u32 function = (address - 0x28) >> 3 & 15;
|
||||
if(function == 0) fractionalCounter[index] =
|
||||
(u32{fractionalCounter[index]} & fractionalLowMask) | u32{data} << 8;
|
||||
if(function == 1) fractionalCounter[index] =
|
||||
((u32{data} & 15) << 16) | (u32{fractionalCounter[index]} & 0xffff);
|
||||
if(function == 2) {
|
||||
fractionalIncrement[index] = data;
|
||||
fractionalCounter[index] = u32{fractionalCounter[index]} & 0x0fff00;
|
||||
}
|
||||
if(function == 3) top[index] = data;
|
||||
if(function == 4) bottom[index] = data;
|
||||
if(function == 5) counter[index] = (u32{counter[index]} & 0x0f00) | data;
|
||||
if(function == 6) {
|
||||
if(index == 0) fastFetch = data == 0;
|
||||
if(index == 1 && parameterPointer < 8) parameter[parameterPointer++] = data;
|
||||
if(index == 2) callFunction(data);
|
||||
if(index >= 5) musicWaveform[index - 5] = data & 0x7f;
|
||||
}
|
||||
if(function == 7) {
|
||||
counter[index]--;
|
||||
displayWrite(counter[index], data);
|
||||
}
|
||||
if(function == 8) counter[index] = ((u32{data} & 15) << 8) | (u32{counter[index]} & 0xff);
|
||||
if(function == 9) {
|
||||
if(index == 0) random = 0x2b435044;
|
||||
if(index >= 1 && index <= 4) random.byte(index - 1) = data;
|
||||
if(index >= 5) musicFrequency[index - 5] = frequency(data);
|
||||
}
|
||||
if(function == 10) {
|
||||
displayWrite(counter[index], data);
|
||||
counter[index]++;
|
||||
}
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
u32 offset = address & 0x0fff;
|
||||
n8 result = program(offset);
|
||||
|
||||
if(fastFetch && ldaImmediate && result < 0x28) offset = result;
|
||||
ldaImmediate = 0;
|
||||
if(offset < 0x28) return readRegister(offset);
|
||||
|
||||
if(offset >= 0x0ff6 && offset <= 0x0ffb) bank = offset - 0x0ff6;
|
||||
if(fastFetch) ldaImmediate = result == 0xa9;
|
||||
return result;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
u32 offset = address & 0x0fff;
|
||||
if(offset >= 0x28 && offset < 0x80) {
|
||||
writeRegister(offset, data);
|
||||
return data;
|
||||
}
|
||||
if(offset >= 0x0ff6 && offset <= 0x0ffb) bank = offset - 0x0ff6;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
ram.fill(0x00);
|
||||
if(rom.size() >= 32_KiB) {
|
||||
for(u32 offset : range(5_KiB)) ram.write(3_KiB + offset, rom.read(27_KiB + offset));
|
||||
}
|
||||
bank = 5;
|
||||
for(auto& item : top) item = 0;
|
||||
for(auto& item : bottom) item = 0;
|
||||
for(auto& item : counter) item = 0;
|
||||
for(auto& item : fractionalCounter) item = 0;
|
||||
for(auto& item : fractionalIncrement) item = 0;
|
||||
for(auto& item : musicCounter) item = 0;
|
||||
for(auto& item : musicFrequency) item = 0;
|
||||
for(auto& item : musicWaveform) item = 0;
|
||||
for(auto& item : parameter) item = 0;
|
||||
random = 0x2b435044;
|
||||
fastFetch = 0;
|
||||
ldaImmediate = 0;
|
||||
parameterPointer = 0;
|
||||
audioPhase = 0;
|
||||
harmony.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(ram);
|
||||
s(bank);
|
||||
for(auto& item : top) s(item);
|
||||
for(auto& item : bottom) s(item);
|
||||
for(auto& item : counter) s(item);
|
||||
for(auto& item : fractionalCounter) s(item);
|
||||
for(auto& item : fractionalIncrement) s(item);
|
||||
for(auto& item : musicCounter) s(item);
|
||||
for(auto& item : musicFrequency) s(item);
|
||||
for(auto& item : musicWaveform) s(item);
|
||||
s(random);
|
||||
s(fastFetch);
|
||||
s(ldaImmediate);
|
||||
for(auto& item : parameter) s(item);
|
||||
s(parameterPointer);
|
||||
s(audioPhase);
|
||||
harmony.serialize(s);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,190 @@
|
||||
struct DPC : Interface, Thread {
|
||||
using Interface::Interface;
|
||||
|
||||
struct Fetcher {
|
||||
n8 top;
|
||||
n8 bottom;
|
||||
n8 low;
|
||||
n3 high;
|
||||
n1 flag;
|
||||
n1 musicMode;
|
||||
n1 oscillatorClock;
|
||||
} fetcher[8];
|
||||
|
||||
Memory::Readable<n8> rom;
|
||||
n1 bank;
|
||||
n8 random;
|
||||
n4 movement;
|
||||
n8 drawLatch;
|
||||
n8 drawOperand;
|
||||
n1 drawCarry;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto clockRandom() -> void {
|
||||
n1 input = !((random.bit(7) ^ random.bit(5)) ^ (random.bit(4) ^ random.bit(3)));
|
||||
random = random << 1 | input;
|
||||
}
|
||||
|
||||
auto checkFlag(u32 index) -> void {
|
||||
if(fetcher[index].low == fetcher[index].top) fetcher[index].flag = 1;
|
||||
if(fetcher[index].low == fetcher[index].bottom) fetcher[index].flag = 0;
|
||||
}
|
||||
|
||||
auto decrement(u32 index) -> void {
|
||||
if(--fetcher[index].low == 0xff) {
|
||||
fetcher[index].high--;
|
||||
if(index >= 5 && fetcher[index].musicMode) fetcher[index].low = fetcher[index].top;
|
||||
}
|
||||
checkFlag(index);
|
||||
}
|
||||
|
||||
auto display(u32 index) -> n8 {
|
||||
auto counter = fetcher[index].high << 8 | fetcher[index].low;
|
||||
return rom.read(8_KiB + (0x7ff - counter));
|
||||
}
|
||||
|
||||
auto music() -> n4 {
|
||||
static constexpr u8 amplitude[] = {0x0, 0x4, 0x5, 0x9, 0x6, 0xa, 0xb, 0xf};
|
||||
u32 state = 0;
|
||||
if(fetcher[5].musicMode && fetcher[5].flag) state |= 1;
|
||||
if(fetcher[6].musicMode && fetcher[6].flag) state |= 2;
|
||||
if(fetcher[7].musicMode && fetcher[7].flag) state |= 4;
|
||||
return amplitude[state];
|
||||
}
|
||||
|
||||
auto reverse(n8 data) -> n8 {
|
||||
n8 result;
|
||||
for(u32 bit : range(8)) result.bit(7 - bit) = data.bit(bit);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto readRegister(n16 address) -> n8 {
|
||||
auto index = address & 7;
|
||||
auto function = address.bit(3, 5);
|
||||
|
||||
if(function == 0) {
|
||||
if(index < 4) return random;
|
||||
if(index <= 5) {
|
||||
drawOperand = drawLatch;
|
||||
auto sum = u32{drawOperand} + u32{fetcher[4].top};
|
||||
drawLatch = sum;
|
||||
drawCarry = sum > 0xff;
|
||||
}
|
||||
return n8{drawCarry ? movement << 4 : 0} | music();
|
||||
}
|
||||
|
||||
checkFlag(index);
|
||||
auto data = display(index);
|
||||
n8 result;
|
||||
if(function == 1) result = data;
|
||||
if(function == 2) result = fetcher[index].flag ? data : n8{0};
|
||||
if(function == 3) result = fetcher[index].flag ? n8{data << 4 | data >> 4} : n8{0};
|
||||
if(function == 4) result = fetcher[index].flag ? reverse(data) : n8{0};
|
||||
if(function == 5) result = fetcher[index].flag ? n8{data >> 1} : n8{0};
|
||||
if(function == 6) result = fetcher[index].flag ? n8{data << 1} : n8{0};
|
||||
if(function == 7) result = fetcher[index].flag ? 0xff : 0x00;
|
||||
|
||||
if(index < 5 || !fetcher[index].oscillatorClock) decrement(index);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto writeRegister(n16 address, n8 data) -> void {
|
||||
auto index = address & 7;
|
||||
auto function = address.bit(3, 5);
|
||||
|
||||
if(function == 0) {
|
||||
fetcher[index].top = data;
|
||||
fetcher[index].flag = 0;
|
||||
}
|
||||
if(function == 1) {
|
||||
fetcher[index].bottom = data;
|
||||
checkFlag(index);
|
||||
}
|
||||
if(function == 2) {
|
||||
fetcher[index].low = index >= 5 && fetcher[index].musicMode ? fetcher[index].top : data;
|
||||
if(index == 4) drawLatch = data;
|
||||
checkFlag(index);
|
||||
}
|
||||
if(function == 3) {
|
||||
fetcher[index].high = data.bit(0, 2);
|
||||
if(index >= 5) {
|
||||
fetcher[index].musicMode = data.bit(4);
|
||||
fetcher[index].oscillatorClock = data.bit(5);
|
||||
if(fetcher[index].musicMode && fetcher[index].low == 0xff) {
|
||||
fetcher[index].low = fetcher[index].top;
|
||||
checkFlag(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(function == 4) movement = data.bit(4, 7);
|
||||
if(function == 6) random = 0;
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
clockRandom();
|
||||
|
||||
if(address == 0x1ff8) bank = 0;
|
||||
if(address == 0x1ff9) bank = 1;
|
||||
if(address <= 0x103f) return readRegister(address);
|
||||
return rom.read(bank * 4_KiB + (address & 0xfff));
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
clockRandom();
|
||||
|
||||
if(address == 0x1ff8) bank = 0;
|
||||
if(address == 0x1ff9) bank = 1;
|
||||
if(address >= 0x1040 && address <= 0x107f) {
|
||||
writeRegister(address, data);
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
for(u32 index = 5; index < 8; index++) {
|
||||
if(fetcher[index].oscillatorClock) decrement(index);
|
||||
}
|
||||
step(1);
|
||||
Thread::synchronize(cpu);
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
Thread::create(20'000, std::bind_front(&DPC::main, this));
|
||||
//Stella latch bank 1.
|
||||
//MAME latch bank 0.
|
||||
bank = 1;
|
||||
random = 0;
|
||||
movement = 0;
|
||||
drawLatch = 0;
|
||||
drawOperand = 0;
|
||||
drawCarry = 0;
|
||||
for(auto& item : fetcher) item = {};
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
Thread::serialize(s);
|
||||
s(bank);
|
||||
s(random);
|
||||
s(movement);
|
||||
s(drawLatch);
|
||||
s(drawOperand);
|
||||
s(drawCarry);
|
||||
for(auto& item : fetcher) {
|
||||
s(item.top);
|
||||
s(item.bottom);
|
||||
s(item.low);
|
||||
s(item.high);
|
||||
s(item.flag);
|
||||
s(item.musicMode);
|
||||
s(item.oscillatorClock);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
struct EconoBanking : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
n1 bank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
bank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
}
|
||||
|
||||
private:
|
||||
auto select(n16 address) -> void {
|
||||
if((address & 0x1840) == 0x0800) bank = 0;
|
||||
if((address & 0x1840) == 0x0840) bank = 1;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
struct EFBase : Interface {
|
||||
EFBase(Cartridge& cartridge, n16 hotspotMask, n16 hotspot, n6 startupBank, bool hasRam = false)
|
||||
: Interface(cartridge), hotspotMask(hotspotMask), hotspot(hotspot), startupBank(startupBank), hasRam(hasRam) {}
|
||||
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
const n16 hotspotMask;
|
||||
const n16 hotspot;
|
||||
const n6 startupBank;
|
||||
const bool hasRam;
|
||||
n6 bank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
bankswitch(address);
|
||||
if(!address.bit(12)) return data;
|
||||
if(hasRam && address <= 0x107f) {
|
||||
ram.write(address & 0x7f, data);
|
||||
return data;
|
||||
}
|
||||
if(hasRam && address <= 0x10ff) return ram.read(address & 0x7f);
|
||||
return rom.read(bank * 0x1000 + (address & 0x0fff));
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
bankswitch(address);
|
||||
if(hasRam && address >= 0x1000 && address <= 0x107f) {
|
||||
ram.write(address & 0x7f, data);
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
bank = startupBank;
|
||||
if(hasRam) ram.allocate(128, 0x00);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
if(hasRam) s(ram);
|
||||
}
|
||||
|
||||
private:
|
||||
auto bankswitch(n16 address) -> void {
|
||||
if((address & hotspotMask) == hotspot) bank = address - hotspot;
|
||||
}
|
||||
};
|
||||
|
||||
struct EF : EFBase {
|
||||
EF(Cartridge& cartridge) : EFBase(cartridge, 0x1ff0, 0x1fe0, 1) {}
|
||||
};
|
||||
|
||||
struct EFF : EFBase {
|
||||
EFF(Cartridge& cartridge) : EFBase(cartridge, 0x1ff0, 0x1fe0, 1) {}
|
||||
|
||||
M24C eeprom;
|
||||
PersistentMemory persistent;
|
||||
|
||||
auto load() -> void override {
|
||||
EFBase::load();
|
||||
persistent.load(pak, "save.eeprom", 2_KiB, 0xff);
|
||||
eeprom.load(M24C::Type::M24C16);
|
||||
std::copy(persistent.memory.begin(), persistent.memory.end(), std::begin(eeprom.memory));
|
||||
}
|
||||
|
||||
auto save() -> void override {
|
||||
persistent.flush(pak);
|
||||
}
|
||||
|
||||
auto unload() -> void override {
|
||||
persistent.flush(pak);
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(address >= 0x1ff0 && address <= 0x1ff4) {
|
||||
if(address == 0x1ff0) setClock(0);
|
||||
if(address == 0x1ff1) setClock(1);
|
||||
if(address == 0x1ff2) setData(0);
|
||||
if(address == 0x1ff3) setData(1);
|
||||
if(address == 0x1ff4) return rom.read(bank * 0x1000 + 0x0ff4 + eeprom.read());
|
||||
}
|
||||
return EFBase::read(address, data);
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(address >= 0x1ff0 && address <= 0x1ff3) {
|
||||
if(address == 0x1ff0) setClock(0);
|
||||
if(address == 0x1ff1) setClock(1);
|
||||
if(address == 0x1ff2) setData(0);
|
||||
if(address == 0x1ff3) setData(1);
|
||||
}
|
||||
return EFBase::write(address, data);
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
EFBase::power(reset);
|
||||
eeprom.power();
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
EFBase::serialize(s);
|
||||
eeprom.serialize(s, false);
|
||||
if(s.reading()) {
|
||||
std::copy(persistent.memory.begin(), persistent.memory.end(), std::begin(eeprom.memory));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
auto synchronizePersistent() -> void {
|
||||
persistent.replace({(const u8*)eeprom.memory, eeprom.size()});
|
||||
}
|
||||
|
||||
auto setClock(bool value) -> void {
|
||||
eeprom.clock = eeprom.clock();
|
||||
eeprom.data = eeprom.data();
|
||||
eeprom.clock = value;
|
||||
eeprom.write();
|
||||
synchronizePersistent();
|
||||
}
|
||||
|
||||
auto setData(bool value) -> void {
|
||||
eeprom.clock = eeprom.clock();
|
||||
eeprom.data = eeprom.data();
|
||||
eeprom.data = value;
|
||||
eeprom.write();
|
||||
synchronizePersistent();
|
||||
}
|
||||
};
|
||||
|
||||
struct DF : EFBase {
|
||||
DF(Cartridge& cartridge) : EFBase(cartridge, 0x1fe0, 0x1fc0, 15) {}
|
||||
};
|
||||
|
||||
struct BF : EFBase {
|
||||
BF(Cartridge& cartridge) : EFBase(cartridge, 0x1fc0, 0x1f80, 1) {}
|
||||
};
|
||||
|
||||
struct EFSC : EFBase {
|
||||
EFSC(Cartridge& cartridge) : EFBase(cartridge, 0x1ff0, 0x1fe0, 15, true) {}
|
||||
};
|
||||
|
||||
struct DFSC : EFBase {
|
||||
DFSC(Cartridge& cartridge) : EFBase(cartridge, 0x1fe0, 0x1fc0, 15, true) {}
|
||||
};
|
||||
|
||||
struct BFSC : EFBase {
|
||||
BFSC(Cartridge& cartridge) : EFBase(cartridge, 0x1fc0, 0x1f80, 15, true) {}
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
struct ELFCartridge : Interface {
|
||||
ELFCartridge(Cartridge& cartridge) : Interface(cartridge) {}
|
||||
|
||||
auto fail(string reason) -> void {
|
||||
runtime.unload();
|
||||
publishedError = std::move(reason);
|
||||
ready = false;
|
||||
cartridge.node->setAttribute("error", publishedError);
|
||||
}
|
||||
|
||||
auto clearError() -> void {
|
||||
publishedError = {};
|
||||
cartridge.node->setAttribute("error");
|
||||
}
|
||||
|
||||
auto publishRuntimeError() -> bool {
|
||||
if(!runtime.faulted()) return false;
|
||||
auto error = runtime.error();
|
||||
if(error != publishedError) {
|
||||
publishedError = std::move(error);
|
||||
cartridge.node->setAttribute("error", publishedError);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto access(n16 address, n8 value) -> n8 {
|
||||
if(!ready || publishRuntimeError()) return value;
|
||||
return runtime.access(address, value);
|
||||
}
|
||||
|
||||
auto load() -> void override {
|
||||
image.clear();
|
||||
object = {};
|
||||
linker = {};
|
||||
ready = false;
|
||||
clearError();
|
||||
if(auto fp = pak->read("program.elf")) {
|
||||
if(fp->size() > ELF::MaximumImageSize) return fail("ELF image exceeds 16 MiB");
|
||||
image.resize(fp->size());
|
||||
fp->read({image.data(), image.size()});
|
||||
} else {
|
||||
return fail("ELF package is missing program.elf");
|
||||
}
|
||||
if(!object.parse(image)) return fail(object.error());
|
||||
if(!linker.link(object, ELF::externalSymbols(Region::NTSC()))) return fail(linker.error());
|
||||
ready = true;
|
||||
}
|
||||
|
||||
auto unload() -> void override {
|
||||
runtime.unload();
|
||||
image.clear();
|
||||
object = {};
|
||||
linker = {};
|
||||
ready = false;
|
||||
publishedError = {};
|
||||
}
|
||||
|
||||
auto power(bool) -> void override {
|
||||
if(!ready) return;
|
||||
if(!linker.link(object, ELF::externalSymbols(Region::NTSC()))) return fail(linker.error());
|
||||
runtime.power(linker.result(), Region::NTSC());
|
||||
clearError();
|
||||
publishRuntimeError();
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 value) -> n8 override {
|
||||
return access(address, value);
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 value) -> n8 override {
|
||||
return access(address, value);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
if(!ready) return;
|
||||
runtime.serialize(s);
|
||||
if(s.reading()) {
|
||||
clearError();
|
||||
publishRuntimeError();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<u8> image;
|
||||
ELF::Object object;
|
||||
ELF::Linker linker;
|
||||
ELF::Runtime runtime;
|
||||
bool ready = false;
|
||||
string publishedError;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
struct Enhanced3F : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
n8 bank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
if(!address.bit(11)) return rom.read(bank * 0x800 + (address & 0x07ff));
|
||||
return rom.read(rom.size() - 0x800 + (address & 0x07ff));
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(address <= 0x003f) bank = data % (rom.size() >> 11);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
//Stella powers bank 0; MAME powers the final bank.
|
||||
bank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
struct FA2 : Interface, Thread {
|
||||
using Interface::Interface;
|
||||
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
PersistentMemory persistent;
|
||||
n3 bank;
|
||||
n9 busyTicks;
|
||||
n1 operationCompleted;
|
||||
n1 operationSucceeded;
|
||||
|
||||
auto load() -> void override {
|
||||
Memory::Readable<n8> image;
|
||||
Interface::load(image, "program.rom");
|
||||
if(image.size() >= 29_KiB) {
|
||||
rom.allocate(28_KiB, 0x00);
|
||||
for(u32 offset : range(28_KiB)) rom.program(offset, image.read(1_KiB + offset));
|
||||
} else {
|
||||
rom.allocate(image.size(), 0x00);
|
||||
for(u32 offset : range(image.size())) rom.program(offset, image.read(offset));
|
||||
}
|
||||
persistent.load(pak, "save.flash", 256, 0x00);
|
||||
}
|
||||
|
||||
auto save() -> void override {
|
||||
persistent.flush(pak);
|
||||
}
|
||||
|
||||
auto unload() -> void override {
|
||||
persistent.flush(pak);
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
if(address == 0x1ff4 && rom.size() == 28_KiB) return operate();
|
||||
bankswitch(address);
|
||||
if(address <= 0x10ff) {
|
||||
ram.write(address & 0xff, data);
|
||||
return data;
|
||||
}
|
||||
if(address <= 0x11ff) return ram.read(address & 0xff);
|
||||
return rom.read(bank * 4_KiB + (address & 0x0fff));
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
if(address == 0x1ff4 && rom.size() == 28_KiB) {
|
||||
operate();
|
||||
return data;
|
||||
}
|
||||
bankswitch(address);
|
||||
if(address <= 0x10ff) ram.write(address & 0xff, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto main() -> void {
|
||||
if(busyTicks && !--busyTicks) {
|
||||
operationCompleted = 1;
|
||||
ram.write(255, operationSucceeded ? 0x00 : 0xff);
|
||||
}
|
||||
step(1);
|
||||
Thread::synchronize(cpu);
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
Thread::create(2'000, std::bind_front(&FA2::main, this));
|
||||
bank = 0;
|
||||
busyTicks = 0;
|
||||
operationCompleted = 0;
|
||||
operationSucceeded = 0;
|
||||
ram.allocate(256, 0x00);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
Thread::serialize(s);
|
||||
s(bank);
|
||||
s(busyTicks);
|
||||
s(operationCompleted);
|
||||
s(operationSucceeded);
|
||||
s(ram);
|
||||
}
|
||||
|
||||
private:
|
||||
auto bankswitch(n16 address) -> void {
|
||||
if(address < 0x1ff5 || address > 0x1ffb) return;
|
||||
auto selected = address - 0x1ff5;
|
||||
if(selected < rom.size() / 4_KiB) bank = selected;
|
||||
}
|
||||
|
||||
auto operate() -> n8 {
|
||||
auto value = rom.read(bank * 4_KiB + 0x0ff4);
|
||||
if(busyTicks) return value | 0x40;
|
||||
if(operationCompleted) {
|
||||
operationCompleted = 0;
|
||||
return value & ~0x40;
|
||||
}
|
||||
|
||||
operationSucceeded = 1;
|
||||
auto command = ram.read(255);
|
||||
if(command == 0) {
|
||||
persistent.erase();
|
||||
busyTicks = 202;
|
||||
} else if(command == 1) {
|
||||
for(u32 offset : range(256)) ram.write(offset, persistent.read(offset));
|
||||
busyTicks = 1;
|
||||
} else if(command == 2) {
|
||||
persistent.replace({(const u8*)ram.data(), 256});
|
||||
busyTicks = 202;
|
||||
} else {
|
||||
operationSucceeded = 0;
|
||||
ram.write(255, 0xff);
|
||||
return value & ~0x40;
|
||||
}
|
||||
return value | 0x40;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
struct FourA50 : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
n16 sliceLow;
|
||||
n16 sliceMiddle;
|
||||
n16 sliceHigh;
|
||||
n1 romLow;
|
||||
n1 romMiddle;
|
||||
n1 romHigh;
|
||||
n16 lastAddress;
|
||||
n8 lastData;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) {
|
||||
checkBankSwitch(address, data);
|
||||
} else if((address & 0x1800) == 0x1000) {
|
||||
data = romLow ? readRom(sliceLow + (address & 0x07ff)) : ram.read(sliceLow + (address & 0x07ff));
|
||||
} else if(address <= 0x1dff) {
|
||||
data = romMiddle ? readRom(0x10000 + sliceMiddle + (address & 0x07ff))
|
||||
: ram.read(sliceMiddle + (address & 0x07ff));
|
||||
} else if(address <= 0x1eff) {
|
||||
data = romHigh ? readRom(0x10000 + sliceHigh + (address & 0x00ff))
|
||||
: ram.read(sliceHigh + (address & 0x00ff));
|
||||
} else {
|
||||
data = readRom(0x1ff00 + (address & 0x00ff));
|
||||
if((lastData & 0xe0) == 0x60 && (lastAddress >= 0x1000 || lastAddress < 0x0200)) {
|
||||
sliceHigh = (sliceHigh & 0xf0ff) | ((address & 0x0008) << 8) | ((address & 0x0070) << 4);
|
||||
}
|
||||
}
|
||||
lastData = data;
|
||||
lastAddress = address;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) {
|
||||
checkBankSwitch(address, data);
|
||||
} else if((address & 0x1800) == 0x1000) {
|
||||
if(!romLow) {
|
||||
ram.write(sliceLow + (address & 0x07ff), data);
|
||||
}
|
||||
} else if(address <= 0x1dff) {
|
||||
if(!romMiddle) {
|
||||
ram.write(sliceMiddle + (address & 0x07ff), data);
|
||||
}
|
||||
} else if(address <= 0x1eff) {
|
||||
if(!romHigh) {
|
||||
ram.write(sliceHigh + (address & 0x00ff), data);
|
||||
}
|
||||
} else if((lastData & 0xe0) == 0x60 && (lastAddress >= 0x1000 || lastAddress < 0x0200)) {
|
||||
sliceHigh = (sliceHigh & 0xf0ff) | ((address & 0x0008) << 8) | ((address & 0x0070) << 4);
|
||||
}
|
||||
lastData = data;
|
||||
lastAddress = address;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
sliceLow = 0;
|
||||
sliceMiddle = 0;
|
||||
sliceHigh = 0;
|
||||
romLow = 1;
|
||||
romMiddle = 1;
|
||||
romHigh = 1;
|
||||
lastAddress = 0xffff;
|
||||
lastData = 0xff;
|
||||
ram.allocate(32_KiB, 0x00);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(ram);
|
||||
s(sliceLow);
|
||||
s(sliceMiddle);
|
||||
s(sliceHigh);
|
||||
s(romLow);
|
||||
s(romMiddle);
|
||||
s(romHigh);
|
||||
s(lastAddress);
|
||||
s(lastData);
|
||||
}
|
||||
|
||||
private:
|
||||
auto readRom(u32 address) -> n8 {
|
||||
return rom.read(address % rom.size());
|
||||
}
|
||||
|
||||
auto bankROMLower(u16 value) -> void { romLow = 1; sliceLow = value << 11; }
|
||||
auto bankRAMLower(u16 value) -> void { romLow = 0; sliceLow = value << 11; }
|
||||
auto bankROMMiddle(u16 value) -> void { romMiddle = 1; sliceMiddle = value << 11; }
|
||||
auto bankRAMMiddle(u16 value) -> void { romMiddle = 0; sliceMiddle = value << 11; }
|
||||
auto bankROMHigh(u16 value) -> void { romHigh = 1; sliceHigh = value << 8; }
|
||||
auto bankRAMHigh(u16 value) -> void { romHigh = 0; sliceHigh = value << 8; }
|
||||
|
||||
auto checkBankSwitch(n16 address, n8 data) -> void {
|
||||
if((lastData & 0xe0) == 0x60 && (lastAddress >= 0x1000 || lastAddress < 0x0200)) {
|
||||
if((address & 0x0f00) == 0x0c00) bankROMHigh(address & 0x00ff);
|
||||
else if((address & 0x0f00) == 0x0d00) bankRAMHigh(address & 0x007f);
|
||||
else if((address & 0x0f40) == 0x0e00) bankROMLower(address & 0x001f);
|
||||
else if((address & 0x0f40) == 0x0e40) bankRAMLower(address & 0x000f);
|
||||
else if((address & 0x0f40) == 0x0f00) bankROMMiddle(address & 0x001f);
|
||||
else if((address & 0x0f50) == 0x0f40) bankRAMMiddle(address & 0x000f);
|
||||
else if((address & 0x0f00) == 0x0400) sliceLow ^= 0x0800;
|
||||
else if((address & 0x0f00) == 0x0500) sliceLow ^= 0x1000;
|
||||
else if((address & 0x0f00) == 0x0800) sliceMiddle ^= 0x0800;
|
||||
else if((address & 0x0f00) == 0x0900) sliceMiddle ^= 0x1000;
|
||||
}
|
||||
|
||||
if((address & 0x0f75) == 0x0074) bankROMHigh(data);
|
||||
else if((address & 0x0f75) == 0x0075) bankRAMHigh(data & 0x7f);
|
||||
else if((address & 0x0f7c) == 0x0078) {
|
||||
if((data & 0xf0) == 0x00) bankROMLower(data & 0x0f);
|
||||
else if((data & 0xf0) == 0x40) bankRAMLower(data & 0x0f);
|
||||
else if((data & 0xf0) == 0x90) bankROMMiddle((data & 0x0f) | 0x10);
|
||||
else if((data & 0xf0) == 0xc0) bankRAMMiddle(data & 0x0f);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
struct Jane : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
n2 bank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
bank = 1;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
}
|
||||
|
||||
private:
|
||||
auto select(n16 address) -> void {
|
||||
if(address == 0x1ff0) bank = 0;
|
||||
if(address == 0x1ff1) bank = 1;
|
||||
if(address == 0x1ff8) bank = 2;
|
||||
if(address == 0x1ff9) bank = 3;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
struct JVP : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
n1 bank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
//Stella powers bank 1; MAME powers bank 0.
|
||||
bank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
}
|
||||
|
||||
private:
|
||||
auto select(n16 address) -> void {
|
||||
//Stella selects directly; MAME toggles either hotspot.
|
||||
if((address & 0x16e0) == 0x06a0) bank = 0;
|
||||
if((address & 0x16e0) == 0x06c0) bank = 1;
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,6 @@
|
||||
struct Linear : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
bool hasRam;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
@@ -14,32 +12,19 @@ struct Linear : Interface {
|
||||
auto unload() -> void override {
|
||||
}
|
||||
|
||||
auto read(n16 address) -> n8 override {
|
||||
if(address.bit(12)) {
|
||||
if(hasRam && address >= 0x1080 && address <= 0x10ff) return ram.read(address & 0x7f);
|
||||
return rom.read(address & 0xfff);
|
||||
}
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
if(address.bit(12)) return rom.read(address & 0xfff);
|
||||
|
||||
return 0xff;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> bool override {
|
||||
if(address >= 0x1000 && address <= 0x107f) {
|
||||
hasRam = true;
|
||||
ram.write(address & 0x7f, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power() -> void override {
|
||||
hasRam = 0;
|
||||
ram.allocate(128);
|
||||
auto power(bool reset) -> void override {
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(hasRam);
|
||||
s(ram);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
struct MNetwork : Interface {
|
||||
using Interface::Interface;
|
||||
static constexpr u8 LowerRam = 7;
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
n3 lowerSelector;
|
||||
n2 upperRamBank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto select(n16 address) -> void {
|
||||
address &= 0x1fff;
|
||||
if(address >= 0x1fe8 && address <= 0x1feb) upperRamBank = address - 0x1fe8;
|
||||
if(address < 0x1fe0 || address > 0x1fe7) return;
|
||||
|
||||
auto selector = address - 0x1fe0;
|
||||
if(selector == 7) {
|
||||
lowerSelector = LowerRam;
|
||||
return;
|
||||
}
|
||||
if(rom.size() == 8_KiB) {
|
||||
if(selector >= 4) lowerSelector = selector - 4;
|
||||
return;
|
||||
}
|
||||
if(rom.size() == 12_KiB) {
|
||||
static constexpr u8 banks[] = {0, 1, 0, 1, 2, 3, 4};
|
||||
lowerSelector = banks[selector];
|
||||
return;
|
||||
}
|
||||
lowerSelector = selector;
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
|
||||
if(address >= 0x1000 && address <= 0x13ff) {
|
||||
if(lowerSelector != LowerRam) return rom.read((lowerSelector * 0x800) + (address & 0x7ff));
|
||||
ram.write(address & 0x3ff, data);
|
||||
return data;
|
||||
}
|
||||
if(address >= 0x1400 && address <= 0x17ff) {
|
||||
if(lowerSelector != LowerRam) return rom.read((lowerSelector * 0x800) + (address & 0x7ff));
|
||||
return ram.read(address & 0x3ff);
|
||||
}
|
||||
if(address >= 0x1800 && address <= 0x18ff) {
|
||||
auto offset = 0x400 + upperRamBank * 0x100 + (address & 0xff);
|
||||
ram.write(offset, data);
|
||||
return data;
|
||||
}
|
||||
if(address >= 0x1900 && address <= 0x19ff) {
|
||||
return ram.read(0x400 + upperRamBank * 0x100 + (address & 0xff));
|
||||
}
|
||||
if(address >= 0x1a00) return rom.read((rom.size() - 0x800) + (address & 0x7ff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
|
||||
if(lowerSelector == LowerRam && address >= 0x1000 && address <= 0x13ff) {
|
||||
ram.write(address & 0x3ff, data);
|
||||
return data;
|
||||
}
|
||||
if(address >= 0x1800 && address <= 0x18ff) {
|
||||
ram.write(0x400 + upperRamBank * 0x100 + (address & 0xff), data);
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
lowerSelector = 0;
|
||||
upperRamBank = 0;
|
||||
ram.allocate(2_KiB, 0x00);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(lowerSelector);
|
||||
s(upperRamBank);
|
||||
s(ram);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
struct MDM : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
n7 bank;
|
||||
n1 bankingDisabled;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
bankswitch(address);
|
||||
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
bankswitch(address & 0x1fff);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
bank = 0;
|
||||
bankingDisabled = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
s(bankingDisabled);
|
||||
}
|
||||
|
||||
private:
|
||||
auto bankswitch(n16 address) -> void {
|
||||
if(bankingDisabled || (address & 0x1c00) != 0x0800) return;
|
||||
auto selector = address & 0x00ff;
|
||||
bank = selector % (rom.size() >> 12);
|
||||
if(selector > 127) bankingDisabled = 1;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
struct MegaBoy : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
n4 bank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
//Stella latch bank 15.
|
||||
//MAME latch bank 0.
|
||||
bank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
}
|
||||
|
||||
private:
|
||||
auto select(n16 address) -> void {
|
||||
if(address == 0x1ff0) bank++;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,702 @@
|
||||
struct MovieCart : Interface {
|
||||
using Interface::Interface;
|
||||
|
||||
static constexpr u32 TitleCycles = 1'000'000;
|
||||
static constexpr u16 StreamRoutine = 0x1080;
|
||||
static constexpr u16 BlankRoutine = 0x1180;
|
||||
static constexpr u16 InputRoutine = 0x1100;
|
||||
static constexpr u16 DifficultyRoutine = 0x1200;
|
||||
static constexpr u16 ConsoleRoutine = 0x1300;
|
||||
static constexpr u16 PulseRoutine = 0x1560;
|
||||
static constexpr u16 LineTrampoline = 0x1070;
|
||||
|
||||
struct Kernel {
|
||||
std::array<u8, 1_KiB> image{};
|
||||
u16 vsyncLines = 0;
|
||||
u16 vblankLines = 0;
|
||||
u16 overscanLines = 0;
|
||||
u16 visibleDone = 0;
|
||||
u16 visibleExit = 0;
|
||||
u16 background = 0;
|
||||
std::array<u16, 5> playfieldColor{};
|
||||
u16 playfield0Left = 0;
|
||||
u16 playfield1Left = 0;
|
||||
u16 playfield2Left = 0;
|
||||
u16 playfield0Right = 0;
|
||||
u16 playfield1Right = 0;
|
||||
u16 playfield2Right = 0;
|
||||
std::array<u16, 2> audioVolume{};
|
||||
} kernel;
|
||||
|
||||
VFS::File stream;
|
||||
std::array<std::array<u8, MVC::FieldSize>, 2> fields{};
|
||||
MVC::FieldDescriptor descriptors[2];
|
||||
std::array<u32, 2> loadedFields = {~0u, ~0u};
|
||||
std::array<bool, 2> blankFields{};
|
||||
std::array<u8, 32> fingerprint{};
|
||||
u64 streamSize = 0;
|
||||
u32 fieldCount = 0;
|
||||
u32 fieldNumber = 0;
|
||||
u16 scanline = 0;
|
||||
n1 activeBuffer = 0;
|
||||
n1 odd = 1;
|
||||
n1 a7 = 0;
|
||||
n1 a10 = 0;
|
||||
n16 a10Pulses = 0;
|
||||
bool streamStarted = false;
|
||||
u8 previousCommand = 0;
|
||||
u8 repeatFields = 0;
|
||||
u8 seekSpeed = 2;
|
||||
u8 muteFields = 0;
|
||||
u8 volume = 15;
|
||||
u8 brightness = 15;
|
||||
u8 osdMode = 0;
|
||||
bool monochrome = false;
|
||||
bool playing = true;
|
||||
enum class TitleState : u8 { Display, Exiting, Stream } titleState = TitleState::Display;
|
||||
u32 titleCycles = 0;
|
||||
bool ready = false;
|
||||
|
||||
auto emit(u16& pc, u8 value) -> void {
|
||||
kernel.image[pc++ & 0x03ff] = value;
|
||||
}
|
||||
|
||||
auto immediate(u16& pc, u8 opcode, u8 value, u16* operand = nullptr) -> void {
|
||||
emit(pc, opcode);
|
||||
if(operand) *operand = pc;
|
||||
emit(pc, value);
|
||||
}
|
||||
|
||||
auto absolute(u16& pc, u8 opcode, u16 address) -> void {
|
||||
emit(pc, opcode);
|
||||
emit(pc, address);
|
||||
emit(pc, address >> 8);
|
||||
}
|
||||
|
||||
auto zeroPage(u16& pc, u8 opcode, u8 address) -> void {
|
||||
emit(pc, opcode);
|
||||
emit(pc, address);
|
||||
}
|
||||
|
||||
auto branch(u16& pc, u8 opcode, u16 target) -> void {
|
||||
emit(pc, opcode);
|
||||
auto origin = pc + 1;
|
||||
emit(pc, (u8)(target - origin));
|
||||
}
|
||||
|
||||
auto branchPlaceholder(u16& pc, u8 opcode) -> u16 {
|
||||
emit(pc, opcode);
|
||||
auto operand = pc;
|
||||
emit(pc, 0);
|
||||
return operand;
|
||||
}
|
||||
|
||||
auto patchBranch(u16 operand, u16 target) -> void {
|
||||
kernel.image[operand & 0x03ff] = (u8)(target - (operand + 1));
|
||||
}
|
||||
|
||||
auto buildKernel() -> void {
|
||||
kernel = {};
|
||||
kernel.image.fill(0xea);
|
||||
u16 pc = 0;
|
||||
|
||||
emit(pc, 0x78); //SEI
|
||||
emit(pc, 0xd8); //CLD
|
||||
immediate(pc, 0xa2, 0xff); //LDX #$ff
|
||||
emit(pc, 0x9a); //TXS
|
||||
immediate(pc, 0xa9, 0x02); //LDA #$02
|
||||
zeroPage(pc, 0x85, 0x01); //STA VBLANK
|
||||
immediate(pc, 0xa9, 0x00); //LDA #$00
|
||||
zeroPage(pc, 0x85, 0x0a); //STA CTRLPF
|
||||
immediate(pc, 0xa9, 0x04); //LDA #pure tone
|
||||
zeroPage(pc, 0x85, 0x15); //STA AUDC0
|
||||
immediate(pc, 0xa9, 0x00); //LDA #frequency
|
||||
zeroPage(pc, 0x85, 0x17); //STA AUDF0
|
||||
|
||||
auto frame = pc;
|
||||
immediate(pc, 0xa9, 0x02); //LDA #$02
|
||||
zeroPage(pc, 0x85, 0x00); //STA VSYNC
|
||||
immediate(pc, 0xa2, 3, &kernel.vsyncLines); //LDX #vsync
|
||||
auto vsyncLoop = pc;
|
||||
absolute(pc, 0x20, BlankRoutine); //JSR blank line
|
||||
emit(pc, 0xca); //DEX
|
||||
branch(pc, 0xd0, vsyncLoop); //BNE
|
||||
immediate(pc, 0xa9, 0x00); //LDA #0
|
||||
zeroPage(pc, 0x85, 0x00); //STA VSYNC
|
||||
immediate(pc, 0xa2, 37, &kernel.vblankLines);//LDX #vblank
|
||||
auto vblankLoop = pc;
|
||||
absolute(pc, 0x20, BlankRoutine); //JSR blank line
|
||||
emit(pc, 0xca); //DEX
|
||||
branch(pc, 0xd0, vblankLoop); //BNE
|
||||
immediate(pc, 0xa9, 0x00); //LDA #0
|
||||
zeroPage(pc, 0x85, 0x01); //STA VBLANK
|
||||
absolute(pc, 0x4c, StreamRoutine); //JMP visible stream
|
||||
auto visibleDone = pc;
|
||||
kernel.visibleExit = visibleDone + 0x1000;
|
||||
immediate(pc, 0xa9, 0x02); //LDA #2
|
||||
zeroPage(pc, 0x85, 0x01); //STA VBLANK
|
||||
absolute(pc, 0x20, InputRoutine); //JSR input sampler
|
||||
immediate(pc, 0xa2, 30, &kernel.overscanLines);//LDX #overscan
|
||||
auto overscanLoop = pc;
|
||||
absolute(pc, 0x20, BlankRoutine); //JSR blank line
|
||||
emit(pc, 0xca); //DEX
|
||||
branch(pc, 0xd0, overscanLoop); //BNE
|
||||
absolute(pc, 0x4c, 0x1000 + frame); //JMP frame
|
||||
|
||||
pc = LineTrampoline & 0x03ff;
|
||||
emit(pc, 0x4c); //JMP visible stream/done
|
||||
kernel.visibleDone = pc;
|
||||
emit(pc, StreamRoutine & 0xff);
|
||||
emit(pc, StreamRoutine >> 8);
|
||||
|
||||
pc = PulseRoutine & 0x03ff;
|
||||
emit(pc, 0x60); //RTS (one A10 pulse)
|
||||
|
||||
pc = StreamRoutine & 0x03ff;
|
||||
zeroPage(pc, 0x85, 0x02); //STA WSYNC
|
||||
immediate(pc, 0xa9, 0x00, &kernel.background);
|
||||
zeroPage(pc, 0x85, 0x09); //STA COLUBK
|
||||
immediate(pc, 0xa9, 0x0e, &kernel.playfieldColor[0]);
|
||||
zeroPage(pc, 0x85, 0x08); //STA COLUPF
|
||||
immediate(pc, 0xa9, 0x00, &kernel.playfield0Left);
|
||||
zeroPage(pc, 0x85, 0x0d); //STA PF0
|
||||
immediate(pc, 0xa9, 0x00, &kernel.playfield1Left);
|
||||
zeroPage(pc, 0x85, 0x0e); //STA PF1
|
||||
immediate(pc, 0xa9, 0x00, &kernel.playfield2Left);
|
||||
zeroPage(pc, 0x85, 0x0f); //STA PF2
|
||||
immediate(pc, 0xa9, 0x00, &kernel.audioVolume[0]);
|
||||
zeroPage(pc, 0x85, 0x19); //STA AUDV0
|
||||
emit(pc, 0xea); //align five color cells
|
||||
immediate(pc, 0xa9, 0x0e, &kernel.playfieldColor[1]);
|
||||
zeroPage(pc, 0x85, 0x08); //STA COLUPF
|
||||
immediate(pc, 0xa9, 0x0e, &kernel.playfieldColor[2]);
|
||||
zeroPage(pc, 0x85, 0x08); //STA COLUPF
|
||||
immediate(pc, 0xa9, 0x00, &kernel.playfield0Right);
|
||||
zeroPage(pc, 0x85, 0x0d); //STA PF0
|
||||
immediate(pc, 0xa9, 0x0e, &kernel.playfieldColor[3]);
|
||||
zeroPage(pc, 0x85, 0x08); //STA COLUPF
|
||||
immediate(pc, 0xa9, 0x00, &kernel.playfield1Right);
|
||||
zeroPage(pc, 0x85, 0x0e); //STA PF1
|
||||
immediate(pc, 0xa9, 0x0e, &kernel.playfieldColor[4]);
|
||||
zeroPage(pc, 0x85, 0x08); //STA COLUPF
|
||||
immediate(pc, 0xa9, 0x00, &kernel.playfield2Right);
|
||||
zeroPage(pc, 0x85, 0x0f); //STA PF2
|
||||
absolute(pc, 0x4c, LineTrampoline); //JMP low-page trampoline
|
||||
|
||||
pc = BlankRoutine & 0x03ff;
|
||||
zeroPage(pc, 0x85, 0x02); //STA WSYNC
|
||||
immediate(pc, 0xa9, 0x00, &kernel.audioVolume[1]);
|
||||
zeroPage(pc, 0x85, 0x19); //STA AUDV0
|
||||
emit(pc, 0x60); //RTS
|
||||
|
||||
pc = InputRoutine & 0x03ff;
|
||||
zeroPage(pc, 0xa5, 0x0c); //LDA INPT4
|
||||
auto noFire = branchPlaceholder(pc, 0x30); //BMI not pressed
|
||||
absolute(pc, 0x20, PulseRoutine);
|
||||
emit(pc, 0x60); //fire = command 1
|
||||
patchBranch(noFire, pc);
|
||||
|
||||
auto joystickCommand = [&](u8 mask, u8 pulses) {
|
||||
absolute(pc, 0xad, 0x0280); //LDA SWCHA
|
||||
immediate(pc, 0x29, mask); //AND direction
|
||||
auto notPressed = branchPlaceholder(pc, 0xd0);
|
||||
for(u32 pulse : range(pulses)) absolute(pc, 0x20, PulseRoutine);
|
||||
emit(pc, 0x60);
|
||||
patchBranch(notPressed, pc);
|
||||
};
|
||||
joystickCommand(0x40, 2); //left = seek backward
|
||||
joystickCommand(0x80, 3); //right = seek forward
|
||||
joystickCommand(0x10, 4); //up = volume up
|
||||
joystickCommand(0x20, 5); //down = volume down
|
||||
absolute(pc, 0x4c, DifficultyRoutine); //JMP difficulty sampler
|
||||
|
||||
pc = DifficultyRoutine & 0x03ff;
|
||||
auto difficultyCommand = [&](u8 mask, u8 pulses) {
|
||||
absolute(pc, 0xad, 0x0282); //LDA SWCHB
|
||||
immediate(pc, 0x29, mask); //AND difficulty switch
|
||||
auto inactive = branchPlaceholder(pc, 0xd0);
|
||||
for(u32 pulse : range(pulses)) absolute(pc, 0x20, PulseRoutine);
|
||||
emit(pc, 0x60);
|
||||
patchBranch(inactive, pc);
|
||||
};
|
||||
difficultyCommand(0x40, 9); //left difficulty = ten seconds back
|
||||
difficultyCommand(0x80, 10); //right difficulty = single step
|
||||
absolute(pc, 0x4c, ConsoleRoutine); //JMP console sampler
|
||||
|
||||
pc = ConsoleRoutine & 0x03ff;
|
||||
auto consoleCommand = [&](u8 mask, u8 pulses) {
|
||||
absolute(pc, 0xad, 0x0282); //LDA SWCHB
|
||||
immediate(pc, 0x29, mask); //AND console switch
|
||||
auto inactive = branchPlaceholder(pc, 0xd0);
|
||||
for(u32 pulse : range(pulses)) absolute(pc, 0x20, PulseRoutine);
|
||||
emit(pc, 0x60);
|
||||
patchBranch(inactive, pc);
|
||||
};
|
||||
consoleCommand(0x02, 6); //select = cycle OSD
|
||||
consoleCommand(0x01, 7); //reset = restart movie
|
||||
consoleCommand(0x08, 8); //B/W = monochrome toggle
|
||||
emit(pc, 0x60);
|
||||
|
||||
kernel.image[0x03fc] = 0x00;
|
||||
kernel.image[0x03fd] = 0x10;
|
||||
kernel.image[0x03fe] = 0x00;
|
||||
kernel.image[0x03ff] = 0x10;
|
||||
}
|
||||
|
||||
auto fail(string reason) -> void {
|
||||
ready = false;
|
||||
cartridge.node->setAttribute("error", reason);
|
||||
}
|
||||
|
||||
auto updateStatus() -> void {
|
||||
cartridge.node->setAttribute("moviecart-field", string{fieldNumber});
|
||||
cartridge.node->setAttribute("moviecart-state", playing ? "playing" : "paused");
|
||||
cartridge.node->setAttribute("moviecart-volume", string{volume});
|
||||
cartridge.node->setAttribute("moviecart-brightness", string{brightness});
|
||||
cartridge.node->setAttribute("moviecart-monochrome", monochrome ? "on" : "off");
|
||||
cartridge.node->setAttribute("moviecart-osd", string{osdMode});
|
||||
cartridge.node->setAttribute("moviecart-seek-speed", string{seekSpeed});
|
||||
cartridge.node->setAttribute("moviecart-repeat", string{repeatFields});
|
||||
cartridge.node->setAttribute("moviecart-mute", string{muteFields});
|
||||
static constexpr const char* titleNames[] = {"display", "exiting", "stream"};
|
||||
cartridge.node->setAttribute("moviecart-title", titleNames[(u32)titleState]);
|
||||
}
|
||||
|
||||
auto readField(u32 number, u32 buffer) -> bool {
|
||||
if(!stream || number >= fieldCount || buffer >= fields.size()) return false;
|
||||
auto offset = (u64)number * MVC::FieldSize;
|
||||
if(offset > streamSize || MVC::FieldSize > streamSize - offset) return false;
|
||||
std::array<u8, MVC::FieldSize> candidate{};
|
||||
stream->seek(offset);
|
||||
stream->read({candidate.data(), candidate.size()});
|
||||
MVC::FieldDescriptor descriptor;
|
||||
if(!MVC::parseField(candidate, descriptor)) {
|
||||
loadedFields[buffer] = number;
|
||||
blankFields[buffer] = true;
|
||||
return false;
|
||||
}
|
||||
fields[buffer] = candidate;
|
||||
descriptors[buffer] = descriptor;
|
||||
loadedFields[buffer] = number;
|
||||
blankFields[buffer] = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto calculateFingerprint() -> void {
|
||||
Hash::SHA256 hash;
|
||||
MVC::fingerprintPrefix(hash, streamSize, fields[0], fields[1]);
|
||||
stream->seek(streamSize - MVC::FieldSize);
|
||||
for(u32 byte : range(MVC::FieldSize)) hash.input(stream->read());
|
||||
auto output = hash.output();
|
||||
for(u32 index : range(fingerprint.size())) fingerprint[index] = output[index];
|
||||
}
|
||||
|
||||
auto load() -> void override {
|
||||
cartridge.node->setAttribute("error");
|
||||
stream = pak->read("stream.mvc");
|
||||
if(!stream) return fail("MovieCart package is missing stream.mvc");
|
||||
streamSize = stream->size();
|
||||
if(streamSize < MVC::MinimumSize || streamSize % MVC::FieldSize) return fail("invalid MovieCart stream size");
|
||||
if(streamSize / MVC::FieldSize > 0xffffffffull) return fail("MovieCart stream has too many fields");
|
||||
fieldCount = streamSize / MVC::FieldSize;
|
||||
if(!readField(0, 0) || !readField(1, 1)) return fail("invalid initial MovieCart field");
|
||||
calculateFingerprint();
|
||||
ready = true;
|
||||
}
|
||||
|
||||
auto unload() -> void override {
|
||||
stream.reset();
|
||||
for(auto& field : fields) field.fill(0);
|
||||
descriptors[0] = {};
|
||||
descriptors[1] = {};
|
||||
loadedFields = {~0u, ~0u};
|
||||
blankFields = {};
|
||||
fingerprint.fill(0);
|
||||
streamSize = 0;
|
||||
fieldCount = 0;
|
||||
ready = false;
|
||||
}
|
||||
|
||||
auto activateField() -> void {
|
||||
auto& descriptor = descriptors[(u32)activeBuffer];
|
||||
scanline = 0;
|
||||
if(!descriptor.valid) return;
|
||||
odd = descriptor.embeddedFrame & 1;
|
||||
kernel.image[kernel.vsyncLines] = descriptor.vsync;
|
||||
kernel.image[kernel.vblankLines] = descriptor.vblank;
|
||||
kernel.image[kernel.overscanLines] = descriptor.overscan;
|
||||
kernel.image[kernel.visibleDone + 0] = StreamRoutine & 0xff;
|
||||
kernel.image[kernel.visibleDone + 1] = StreamRoutine >> 8;
|
||||
if(blankFields[(u32)activeBuffer]) {
|
||||
cartridge.node->setAttribute("error", string{"invalid MovieCart field ", fieldNumber});
|
||||
} else {
|
||||
cartridge.node->setAttribute("error");
|
||||
}
|
||||
patchLeft();
|
||||
}
|
||||
|
||||
auto clearLeft() -> void {
|
||||
kernel.image[kernel.background] = 0;
|
||||
for(u32 cell : range(3)) kernel.image[kernel.playfieldColor[cell]] = 0;
|
||||
kernel.image[kernel.playfield0Left] = 0;
|
||||
kernel.image[kernel.playfield1Left] = 0;
|
||||
kernel.image[kernel.playfield2Left] = 0;
|
||||
}
|
||||
|
||||
auto clearRight() -> void {
|
||||
for(u32 cell : range(3, 5)) kernel.image[kernel.playfieldColor[cell]] = 0;
|
||||
kernel.image[kernel.playfield0Right] = 0;
|
||||
kernel.image[kernel.playfield1Right] = 0;
|
||||
kernel.image[kernel.playfield2Right] = 0;
|
||||
}
|
||||
|
||||
static auto reverseByte(u8 value) -> u8 {
|
||||
value = (value & 0x55) << 1 | (value & 0xaa) >> 1;
|
||||
value = (value & 0x33) << 2 | (value & 0xcc) >> 2;
|
||||
return value << 4 | value >> 4;
|
||||
}
|
||||
|
||||
static auto reverseNybble(u8 value) -> u8 {
|
||||
value &= 15;
|
||||
return ((value & 1) << 3) | ((value & 2) << 1) | ((value & 4) >> 1) | ((value & 8) >> 3);
|
||||
}
|
||||
|
||||
auto adjustColor(u8 color) const -> u8 {
|
||||
auto luminance = ((color & 0x0e) * brightness / 15) & 0x0e;
|
||||
return (monochrome ? 0 : color & 0xf0) | luminance;
|
||||
}
|
||||
|
||||
static auto titlePixel(u32 x, u32 y) -> bool {
|
||||
auto letterM = (x == 2 || x == 11) || (y < 5 && (x == 2 + y || x == 11 - y));
|
||||
auto letterV = x == 15 + y / 2 || x == 24 - y / 2;
|
||||
auto letterC = (x == 28 && y > 0 && y < 11) || ((y == 0 || y == 11) && x >= 29 && x <= 37);
|
||||
return letterM || letterV || letterC;
|
||||
}
|
||||
|
||||
auto titleByte(u32 visibleLine, u32 cell, u32 visibleLines) const -> u8 {
|
||||
static constexpr u32 height = 12;
|
||||
auto presentationLines = Region::NTSC() ? 192u : 228u;
|
||||
auto top = (presentationLines - height) / 2;
|
||||
if(visibleLines < top + height) top = visibleLines > height ? visibleLines - height : 0;
|
||||
if(visibleLine < top || visibleLine >= top + height) return 0;
|
||||
auto y = visibleLine - top;
|
||||
u8 output = 0;
|
||||
for(u32 bit : range(8)) if(titlePixel(cell * 8 + bit, y)) output |= 0x80 >> bit;
|
||||
return output;
|
||||
}
|
||||
|
||||
auto patchLeft() -> void {
|
||||
auto buffer = (u32)activeBuffer;
|
||||
auto& descriptor = descriptors[buffer];
|
||||
if(blankFields[buffer] || !descriptor.valid || scanline >= descriptor.audioCount) {
|
||||
clearLeft();
|
||||
for(auto operand : kernel.audioVolume) kernel.image[operand] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
auto visibleStart = (u32)descriptor.vsync + descriptor.vblank;
|
||||
if(titleState != TitleState::Stream) {
|
||||
for(auto operand : kernel.audioVolume) kernel.image[operand] = 0;
|
||||
if(scanline < visibleStart || scanline >= visibleStart + descriptor.visible) return clearLeft();
|
||||
auto visibleLine = scanline - visibleStart;
|
||||
auto graph0 = titleByte(visibleLine, 0, descriptor.visible);
|
||||
auto graph1 = titleByte(visibleLine, 1, descriptor.visible);
|
||||
auto graph2 = titleByte(visibleLine, 2, descriptor.visible);
|
||||
kernel.image[kernel.background] = adjustColor(((visibleLine / 12 + 1) & 15) << 4 | 0x02);
|
||||
for(u32 cell : range(3)) kernel.image[kernel.playfieldColor[cell]] = adjustColor(0x0e);
|
||||
kernel.image[kernel.playfield0Left] = reverseNybble(graph0 >> 4) << 4;
|
||||
kernel.image[kernel.playfield1Left] = graph0 << 4 | graph1 >> 4;
|
||||
kernel.image[kernel.playfield2Left] = reverseByte(graph1 << 4 | graph2 >> 4);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& field = fields[buffer];
|
||||
auto audio = muteFields ? 0 : (field[descriptor.audioOffset + scanline] & 15) * volume / 15;
|
||||
for(auto operand : kernel.audioVolume) kernel.image[operand] = audio;
|
||||
if(scanline < visibleStart || scanline >= visibleStart + descriptor.visible) {
|
||||
clearLeft();
|
||||
return;
|
||||
}
|
||||
|
||||
auto visibleLine = scanline - visibleStart;
|
||||
auto overlay = osdMode && visibleLine < descriptor.timecodeCount / 5;
|
||||
auto graph = overlay ? descriptor.timecodeOffset + visibleLine * 5
|
||||
: descriptor.graphOffset + visibleLine * 5;
|
||||
auto color = descriptor.colorOffset + visibleLine * 5;
|
||||
auto backgroundLine = min<u32>(visibleLine + !odd, descriptor.backgroundCount - 1);
|
||||
auto graph0 = field[graph + 0];
|
||||
auto graph1 = field[graph + 1];
|
||||
auto graph2 = field[graph + 2];
|
||||
kernel.image[kernel.background] = overlay ? 0 : adjustColor(field[descriptor.backgroundOffset + backgroundLine]);
|
||||
for(u32 cell : range(3)) {
|
||||
kernel.image[kernel.playfieldColor[cell]] = overlay ? adjustColor(0x0e) : adjustColor(field[color + cell]);
|
||||
}
|
||||
kernel.image[kernel.playfield0Left] = reverseNybble(graph0 >> 4) << 4;
|
||||
kernel.image[kernel.playfield1Left] = graph0 << 4 | graph1 >> 4;
|
||||
kernel.image[kernel.playfield2Left] = reverseByte(graph1 << 4 | graph2 >> 4);
|
||||
}
|
||||
|
||||
auto patchRight() -> void {
|
||||
auto buffer = (u32)activeBuffer;
|
||||
auto& descriptor = descriptors[buffer];
|
||||
auto visibleStart = (u32)descriptor.vsync + descriptor.vblank;
|
||||
if(blankFields[buffer] || !descriptor.valid || scanline < visibleStart
|
||||
|| scanline >= visibleStart + descriptor.visible) {
|
||||
clearRight();
|
||||
return;
|
||||
}
|
||||
if(titleState != TitleState::Stream) {
|
||||
auto visibleLine = scanline - visibleStart;
|
||||
auto graph2 = titleByte(visibleLine, 2, descriptor.visible);
|
||||
kernel.image[kernel.playfield0Right] = reverseNybble(graph2) << 4;
|
||||
kernel.image[kernel.playfield1Right] = titleByte(visibleLine, 3, descriptor.visible);
|
||||
kernel.image[kernel.playfield2Right] = reverseByte(titleByte(visibleLine, 4, descriptor.visible));
|
||||
kernel.image[kernel.playfieldColor[3]] = adjustColor(0x0e);
|
||||
kernel.image[kernel.playfieldColor[4]] = adjustColor(0x0e);
|
||||
return;
|
||||
}
|
||||
auto& field = fields[buffer];
|
||||
auto visibleLine = scanline - visibleStart;
|
||||
auto overlay = osdMode && visibleLine < descriptor.timecodeCount / 5;
|
||||
auto graph = overlay ? descriptor.timecodeOffset + visibleLine * 5
|
||||
: descriptor.graphOffset + visibleLine * 5;
|
||||
auto color = descriptor.colorOffset + visibleLine * 5;
|
||||
auto graph2 = field[graph + 2];
|
||||
kernel.image[kernel.playfield0Right] = reverseNybble(graph2) << 4;
|
||||
kernel.image[kernel.playfield1Right] = field[graph + 3];
|
||||
kernel.image[kernel.playfield2Right] = reverseByte(field[graph + 4]);
|
||||
kernel.image[kernel.playfieldColor[3]] = overlay ? adjustColor(0x0e) : adjustColor(field[color + 3]);
|
||||
kernel.image[kernel.playfieldColor[4]] = overlay ? adjustColor(0x0e) : adjustColor(field[color + 4]);
|
||||
}
|
||||
|
||||
auto advanceField() -> void {
|
||||
if(titleState != TitleState::Stream) {
|
||||
a10Pulses = 0;
|
||||
previousCommand = 0;
|
||||
fieldNumber = 0;
|
||||
activeBuffer = loadedFields[0] == 0 ? 0 : 1;
|
||||
activateField();
|
||||
updateStatus();
|
||||
return;
|
||||
}
|
||||
auto command = min<u32>(a10Pulses, 10);
|
||||
a10Pulses = 0;
|
||||
if(muteFields) muteFields--;
|
||||
s32 seek = 0;
|
||||
bool restart = false;
|
||||
bool step = false;
|
||||
auto fresh = command != previousCommand;
|
||||
if(command == 2 || command == 3) {
|
||||
if(fresh) {
|
||||
repeatFields = 0;
|
||||
seekSpeed = 2;
|
||||
} else {
|
||||
if(repeatFields < 255) repeatFields++;
|
||||
if(repeatFields && repeatFields % 30 == 0) seekSpeed = min<u32>(seekSpeed * 2, 64);
|
||||
}
|
||||
seek = command == 2 ? -(s32)seekSpeed : +(s32)seekSpeed;
|
||||
} else {
|
||||
repeatFields = 0;
|
||||
seekSpeed = 2;
|
||||
}
|
||||
if(fresh) {
|
||||
if(command == 1) playing = !playing;
|
||||
if(command == 4) {
|
||||
if(osdMode == 2 && brightness < 15) brightness++;
|
||||
else if(osdMode != 2 && volume < 15) volume++;
|
||||
}
|
||||
if(command == 5) {
|
||||
if(osdMode == 2 && brightness > 0) brightness--;
|
||||
else if(osdMode != 2 && volume > 0) volume--;
|
||||
}
|
||||
if(command == 6) osdMode = (osdMode + 1) % 3;
|
||||
if(command == 7) {
|
||||
restart = true;
|
||||
playing = true;
|
||||
}
|
||||
if(command == 8) monochrome = !monochrome;
|
||||
if(command == 9) seek = -(s32)descriptors[(u32)activeBuffer].rate * 10;
|
||||
if(command == 10) {
|
||||
playing = false;
|
||||
step = true;
|
||||
}
|
||||
}
|
||||
previousCommand = command;
|
||||
|
||||
s64 requested = fieldNumber;
|
||||
if(restart) requested = 0;
|
||||
else if(seek) requested += seek;
|
||||
else if(step) requested++;
|
||||
else if(playing) requested++;
|
||||
while(requested < 0) requested += 2;
|
||||
auto reachedEOF = requested >= fieldCount;
|
||||
while(requested >= fieldCount) requested -= 2;
|
||||
if(reachedEOF) {
|
||||
repeatFields = 0;
|
||||
seekSpeed = 2;
|
||||
muteFields = 2;
|
||||
}
|
||||
auto next = (u32)requested;
|
||||
auto targetBuffer = next == fieldNumber ? (u32)activeBuffer : (u32)!activeBuffer;
|
||||
if(loadedFields[targetBuffer] != next) readField(next, targetBuffer);
|
||||
fieldNumber = next;
|
||||
activeBuffer = targetBuffer;
|
||||
auto lookahead = fieldNumber + 1;
|
||||
if(lookahead >= fieldCount) lookahead = fieldCount - 2;
|
||||
auto lookaheadBuffer = (u32)!activeBuffer;
|
||||
if(loadedFields[lookaheadBuffer] != lookahead) readField(lookahead, lookaheadBuffer);
|
||||
activateField();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
auto process(n16 address) -> void {
|
||||
if(titleState == TitleState::Display && ++titleCycles >= TitleCycles) titleState = TitleState::Exiting;
|
||||
auto nextA10 = address.bit(10);
|
||||
if(!a10 && nextA10 && !address.bit(7)) a10Pulses++;
|
||||
a10 = nextA10;
|
||||
auto nextA7 = address.bit(7);
|
||||
if(!streamStarted) {
|
||||
auto routine = address & 0x03ff;
|
||||
if(nextA7 && (routine == (StreamRoutine & 0x03ff) || routine == (BlankRoutine & 0x03ff))) {
|
||||
streamStarted = true;
|
||||
patchRight();
|
||||
}
|
||||
a7 = nextA7;
|
||||
return;
|
||||
}
|
||||
if(titleState == TitleState::Exiting && !a7 && nextA7) {
|
||||
titleState = TitleState::Stream;
|
||||
fieldNumber = 0;
|
||||
activeBuffer = loadedFields[0] == 0 ? 0 : 1;
|
||||
activateField();
|
||||
updateStatus();
|
||||
}
|
||||
if(!a7 && nextA7) patchRight();
|
||||
if(a7 && !nextA7) {
|
||||
scanline++;
|
||||
auto& descriptor = descriptors[(u32)activeBuffer];
|
||||
auto totalLines = (u32)descriptor.vsync + descriptor.vblank + descriptor.visible + descriptor.overscan;
|
||||
if(!descriptor.valid || scanline >= totalLines) advanceField();
|
||||
else {
|
||||
patchLeft();
|
||||
auto visibleEnd = (u32)descriptor.vsync + descriptor.vblank + descriptor.visible;
|
||||
if(scanline == visibleEnd) {
|
||||
kernel.image[kernel.visibleDone + 0] = kernel.visibleExit & 0xff;
|
||||
kernel.image[kernel.visibleDone + 1] = kernel.visibleExit >> 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
a7 = nextA7;
|
||||
}
|
||||
|
||||
auto power(bool) -> void override {
|
||||
if(!ready) return;
|
||||
buildKernel();
|
||||
if(!readField(0, 0) || !readField(1, 1)) return fail("MovieCart stream changed while attached");
|
||||
fieldNumber = 0;
|
||||
activeBuffer = 0;
|
||||
odd = 0;
|
||||
a7 = 0;
|
||||
a10 = 0;
|
||||
a10Pulses = 0;
|
||||
streamStarted = false;
|
||||
previousCommand = 0;
|
||||
repeatFields = 0;
|
||||
seekSpeed = 2;
|
||||
muteFields = 0;
|
||||
volume = 15;
|
||||
brightness = 15;
|
||||
osdMode = 0;
|
||||
monochrome = false;
|
||||
playing = true;
|
||||
titleState = TitleState::Display;
|
||||
titleCycles = 0;
|
||||
activateField();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!ready || !address.bit(12)) return data;
|
||||
process(address);
|
||||
return kernel.image[address & 0x03ff];
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(ready && address.bit(12)) process(address);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto serializeDescriptor(serializer& s, MVC::FieldDescriptor& descriptor) -> void {
|
||||
s(descriptor.valid);
|
||||
s(descriptor.extended);
|
||||
s(descriptor.signatureOffset);
|
||||
s(descriptor.vsync);
|
||||
s(descriptor.vblank);
|
||||
s(descriptor.overscan);
|
||||
s(descriptor.visible);
|
||||
s(descriptor.rate);
|
||||
s(descriptor.embeddedFrame);
|
||||
s(descriptor.audioOffset);
|
||||
s(descriptor.audioCount);
|
||||
s(descriptor.graphOffset);
|
||||
s(descriptor.graphCount);
|
||||
s(descriptor.colorOffset);
|
||||
s(descriptor.colorCount);
|
||||
s(descriptor.backgroundOffset);
|
||||
s(descriptor.backgroundCount);
|
||||
s(descriptor.timecodeOffset);
|
||||
s(descriptor.timecodeCount);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
if(!ready) return;
|
||||
auto savedSize = streamSize;
|
||||
auto savedFingerprint = fingerprint;
|
||||
s(std::span<u8>{kernel.image.data(), kernel.image.size()});
|
||||
for(auto& field : fields) s(std::span<u8>{field.data(), field.size()});
|
||||
serializeDescriptor(s, descriptors[0]);
|
||||
serializeDescriptor(s, descriptors[1]);
|
||||
for(auto& loadedField : loadedFields) s(loadedField);
|
||||
for(auto& blankField : blankFields) s(blankField);
|
||||
s(std::span<u8>{savedFingerprint.data(), savedFingerprint.size()});
|
||||
s(savedSize);
|
||||
s(fieldNumber);
|
||||
s(scanline);
|
||||
s(activeBuffer);
|
||||
s(odd);
|
||||
s(a7);
|
||||
s(a10);
|
||||
s(a10Pulses);
|
||||
s(streamStarted);
|
||||
s(previousCommand);
|
||||
s(repeatFields);
|
||||
s(seekSpeed);
|
||||
s(muteFields);
|
||||
s(volume);
|
||||
s(brightness);
|
||||
s(osdMode);
|
||||
s(monochrome);
|
||||
s(playing);
|
||||
s(titleState);
|
||||
s(titleCycles);
|
||||
if(s.reading()) {
|
||||
if(savedSize != streamSize || savedFingerprint != fingerprint) return fail("MovieCart state media mismatch");
|
||||
MVC::FieldDescriptor parsed[2];
|
||||
if(!MVC::parseField(fields[0], parsed[0]) || !MVC::parseField(fields[1], parsed[1])
|
||||
|| parsed[0] != descriptors[0] || parsed[1] != descriptors[1]) {
|
||||
return fail("invalid MovieCart field state");
|
||||
}
|
||||
auto& descriptor = descriptors[(u32)activeBuffer];
|
||||
auto totalLines = (u32)descriptor.vsync + descriptor.vblank + descriptor.visible + descriptor.overscan;
|
||||
if(fieldNumber >= fieldCount || loadedFields[0] >= fieldCount || loadedFields[1] >= fieldCount
|
||||
|| loadedFields[(u32)activeBuffer] != fieldNumber || scanline >= totalLines
|
||||
|| previousCommand > 10 || !seekSpeed || seekSpeed > 64 || volume > 15 || brightness > 15 || osdMode > 2
|
||||
|| (u32)titleState > (u32)TitleState::Stream || titleCycles > TitleCycles) {
|
||||
return fail("invalid MovieCart cursor state");
|
||||
}
|
||||
updateStatus();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
struct ParkerBros03E0 : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
n3 chunks[3];
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
if(address.bit(12)) {
|
||||
u32 segment = address.bit(10, 11);
|
||||
u32 chunk = 7;
|
||||
if(segment < 3) chunk = chunks[segment];
|
||||
return rom.read(chunk * 0x400 + (address & 0x03ff));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
chunks[0] = 4;
|
||||
chunks[1] = 5;
|
||||
chunks[2] = 6;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(chunks[0]);
|
||||
s(chunks[1]);
|
||||
s(chunks[2]);
|
||||
}
|
||||
|
||||
private:
|
||||
auto select(n16 address) -> void {
|
||||
if(address < 0x0380 || address > 0x03ff) return;
|
||||
auto chunk = address & 7;
|
||||
if(!address.bit(4)) chunks[0] = chunk;
|
||||
if(!address.bit(5)) chunks[1] = chunk;
|
||||
if(!address.bit(6)) chunks[2] = chunk;
|
||||
}
|
||||
};
|
||||
@@ -13,7 +13,7 @@ struct ParkerBros : Interface {
|
||||
auto unload() -> void override {
|
||||
}
|
||||
|
||||
auto read(n16 address) -> n8 override {
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
bankswitch(address);
|
||||
|
||||
if(address.bit(12)) {
|
||||
@@ -23,12 +23,12 @@ struct ParkerBros : Interface {
|
||||
if(address >= 0x1c00 && address <= 0x1fff) return rom.read(( 7 * 0x400) + (address & 0x3ff));
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> bool override {
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
bankswitch(address);
|
||||
return false;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto bankswitch(n16 address) -> void {
|
||||
@@ -37,7 +37,7 @@ struct ParkerBros : Interface {
|
||||
if(address >= 0x1ff0 && address <= 0x1ff7) banks[2] = address - 0x1ff0;
|
||||
}
|
||||
|
||||
auto power() -> void override {
|
||||
auto power(bool reset) -> void override {
|
||||
banks[0] = 0;
|
||||
banks[1] = 1;
|
||||
banks[2] = 2;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
struct SaraRam {
|
||||
SaraRam(bool present = false) : present(present) {}
|
||||
|
||||
auto readable(n16 address) -> bool {
|
||||
return present && address >= 0x1000 && address <= 0x10ff;
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 {
|
||||
if(address <= 0x107f) {
|
||||
ram.write(address & 0x7f, data);
|
||||
return data;
|
||||
}
|
||||
return ram.read(address & 0x7f);
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> bool {
|
||||
if(!present || address < 0x1000 || address > 0x107f) return false;
|
||||
ram.write(address & 0x7f, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto power() -> void {
|
||||
if(present) ram.allocate(128, 0x00);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void {
|
||||
if(present) s(ram);
|
||||
}
|
||||
|
||||
const bool present;
|
||||
Memory::Writable<n8> ram;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
struct Superbanking : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
n6 bank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
select(address);
|
||||
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
select(address & 0x1fff);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
bank = (rom.size() >> 12) - 1;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
}
|
||||
|
||||
private:
|
||||
auto select(n16 address) -> void {
|
||||
if((address & 0x1800) == 0x0800) bank = address & ((rom.size() >> 12) - 1);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
struct ThreeEPlus : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
n1 ramSelected[4];
|
||||
n6 bank[4];
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
|
||||
auto segment = address.bit(10, 11);
|
||||
auto offset = address & 0x03ff;
|
||||
if(!ramSelected[segment]) return rom.read((u32)(bank[segment] % (rom.size() >> 10)) * 0x400 + offset);
|
||||
|
||||
auto ramOffset = (u32)bank[segment] * 0x200 + (offset & 0x01ff);
|
||||
if(!(offset & 0x0200)) return ram.read(ramOffset);
|
||||
ram.write(ramOffset, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(address == 0x003e || address == 0x003f) {
|
||||
auto segment = data.bit(6, 7);
|
||||
ramSelected[segment] = address == 0x003e;
|
||||
bank[segment] = data.bit(0, 5);
|
||||
return data;
|
||||
}
|
||||
|
||||
if(!address.bit(12)) return data;
|
||||
auto segment = address.bit(10, 11);
|
||||
auto offset = address & 0x03ff;
|
||||
if(ramSelected[segment] && (offset & 0x0200)) {
|
||||
ram.write((u32)bank[segment] * 0x200 + (offset & 0x01ff), data);
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
for(auto segment : range(4)) {
|
||||
ramSelected[segment] = 0;
|
||||
bank[segment] = 0;
|
||||
}
|
||||
ram.allocate(32_KiB, 0x00);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
for(auto segment : range(4)) {
|
||||
s(ramSelected[segment]);
|
||||
s(bank[segment]);
|
||||
}
|
||||
s(ram);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
struct ThreeE : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
n1 ramSelected;
|
||||
n8 bank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
if(address.bit(11)) return rom.read(rom.size() - 0x800 + (address & 0x07ff));
|
||||
if(!ramSelected) return rom.read(bank * 0x800 + (address & 0x07ff));
|
||||
|
||||
auto offset = bank * 0x400 + (address & 0x03ff);
|
||||
if(!address.bit(10)) return ram.read(offset);
|
||||
ram.write(offset, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(address == 0x003e) {
|
||||
ramSelected = 1;
|
||||
bank = data & 31;
|
||||
return data;
|
||||
}
|
||||
if(address == 0x003f) {
|
||||
ramSelected = 0;
|
||||
bank = data % (rom.size() >> 11);
|
||||
return data;
|
||||
}
|
||||
if(ramSelected && address >= 0x1400 && address <= 0x17ff) {
|
||||
ram.write(bank * 0x400 + (address & 0x03ff), data);
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
//Stella powers bank 0 and MAME the final bank.
|
||||
//Boulder Dash Demo 2 selects bank 1 before using the lower window.
|
||||
ramSelected = 0;
|
||||
bank = 1;
|
||||
ram.allocate(32_KiB, 0x00);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(ramSelected);
|
||||
s(bank);
|
||||
s(ram);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
struct ThreeEX : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
n1 ramSelected;
|
||||
n8 bank;
|
||||
u16 ramBankCount = 1;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(!address.bit(12)) return data;
|
||||
if(address.bit(11)) return rom.read(rom.size() - 0x800 + (address & 0x07ff));
|
||||
if(!ramSelected) return rom.read(bank * 0x800 + (address & 0x07ff));
|
||||
|
||||
auto offset = bank * 0x400 + (address & 0x03ff);
|
||||
if(!address.bit(10)) return ram.read(offset);
|
||||
ram.write(offset, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
if(address == 0x003e) {
|
||||
ramSelected = 1;
|
||||
bank = data % ramBankCount;
|
||||
return data;
|
||||
}
|
||||
if(address == 0x003f) {
|
||||
ramSelected = 0;
|
||||
bank = data % (rom.size() >> 11);
|
||||
return data;
|
||||
}
|
||||
if(ramSelected && address >= 0x1400 && address <= 0x17ff) {
|
||||
ram.write(bank * 0x400 + (address & 0x03ff), data);
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
ramSelected = 0;
|
||||
bank = 1;
|
||||
ramBankCount = (u16)rom.read(rom.size() - 6);
|
||||
ramBankCount++;
|
||||
ram.allocate(ramBankCount * 1_KiB, 0x00);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(ramSelected);
|
||||
s(bank);
|
||||
s(ram);
|
||||
}
|
||||
};
|
||||
@@ -14,23 +14,23 @@ struct Tigervision : Interface {
|
||||
auto unload() -> void override {
|
||||
}
|
||||
|
||||
auto read(n16 address) -> n8 override {
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
if(address.bit(12)) {
|
||||
if(bank != nextBank) bank = nextBank;
|
||||
if(address <= 0x17ff) return rom.read((bank * 0x800) + (address & 0x7ff));
|
||||
return rom.read((rom.size() - 0x800) + (address & 0x7ff));
|
||||
}
|
||||
|
||||
return 0xff;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> bool override {
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
if(address.bit(12) && bank != nextBank) bank = nextBank;
|
||||
if(address <= 0x3f) nextBank = data;
|
||||
return false;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power() -> void override {
|
||||
auto power(bool reset) -> void override {
|
||||
bank = 0;
|
||||
nextBank = 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
struct UA8k : Interface {
|
||||
UA8k(Cartridge& cartridge, bool swapped = false) : Interface(cartridge), swapped(swapped) {}
|
||||
Memory::Readable<n8> rom;
|
||||
const bool swapped;
|
||||
n1 bank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto save() -> void override {
|
||||
}
|
||||
|
||||
auto unload() -> void override {
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
bankswitch(address);
|
||||
if(address.bit(12)) return rom.read((bank * 0x1000) + (address & 0x0fff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
bankswitch(address);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
bank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
}
|
||||
|
||||
private:
|
||||
auto bankswitch(n16 address) -> void {
|
||||
if((address & 0x1260) == 0x0220) bank = swapped ? 1 : 0;
|
||||
if((address & 0x1260) == 0x0240) bank = swapped ? 0 : 1;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
struct Wickstead : Interface {
|
||||
using Interface::Interface;
|
||||
Memory::Readable<n8> rom;
|
||||
Memory::Writable<n8> ram;
|
||||
n3 arrangement;
|
||||
n3 pendingArrangement;
|
||||
u8 delay;
|
||||
bool pending;
|
||||
|
||||
static constexpr u8 arrangements[8][4] = {
|
||||
{0, 0, 1, 3},
|
||||
{0, 1, 2, 3},
|
||||
{4, 5, 6, 7},
|
||||
{7, 4, 2, 3},
|
||||
{0, 0, 6, 7},
|
||||
{0, 1, 7, 6},
|
||||
{2, 3, 4, 5},
|
||||
{6, 0, 5, 1},
|
||||
};
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
advance(true);
|
||||
|
||||
if(address >= 0x0030 && address <= 0x003f) {
|
||||
pendingArrangement = address & 7;
|
||||
delay = 3;
|
||||
pending = true;
|
||||
return data;
|
||||
}
|
||||
if(address >= 0x1000 && address <= 0x103f) return ram.read(address & 0x3f);
|
||||
if(address >= 0x1040 && address <= 0x107f) {
|
||||
ram.write(address & 0x3f, data);
|
||||
return data;
|
||||
}
|
||||
if(address.bit(12)) {
|
||||
auto segment = (address >> 10) & 3;
|
||||
auto chunk = arrangements[arrangement][segment];
|
||||
return rom.read(chunk * 1_KiB + (address & 0x3ff));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
advance(false);
|
||||
if(address >= 0x1040 && address <= 0x107f) {
|
||||
ram.write(address & 0x3f, data);
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
arrangement = 0;
|
||||
pendingArrangement = 0;
|
||||
delay = 0;
|
||||
pending = false;
|
||||
ram.allocate(64, 0x00);
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(arrangement);
|
||||
s(pendingArrangement);
|
||||
s(delay);
|
||||
s(pending);
|
||||
s(ram);
|
||||
}
|
||||
|
||||
private:
|
||||
auto advance(bool isRead) -> void {
|
||||
if(!pending) return;
|
||||
if(delay) {
|
||||
delay--;
|
||||
return;
|
||||
}
|
||||
if(isRead) {
|
||||
arrangement = pendingArrangement;
|
||||
pending = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
struct X07 : Interface {
|
||||
using Interface::Interface;
|
||||
|
||||
Memory::Readable<n8> rom;
|
||||
n4 bank;
|
||||
|
||||
auto load() -> void override {
|
||||
Interface::load(rom, "program.rom");
|
||||
}
|
||||
|
||||
auto read(n16 address, n8 data) -> n8 override {
|
||||
address &= 0x1fff;
|
||||
bankswitch(address);
|
||||
if(address.bit(12)) return rom.read(bank * 0x1000 + (address & 0x0fff));
|
||||
return data;
|
||||
}
|
||||
|
||||
auto write(n16 address, n8 data) -> n8 override {
|
||||
bankswitch(address & 0x1fff);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto power(bool reset) -> void override {
|
||||
bank = 0;
|
||||
}
|
||||
|
||||
auto serialize(serializer& s) -> void override {
|
||||
s(bank);
|
||||
}
|
||||
|
||||
private:
|
||||
auto bankswitch(n16 address) -> void {
|
||||
if((address & 0x180f) == 0x080d) {
|
||||
bank = address.bit(4, 7);
|
||||
} else if((address & 0x1880) == 0 && bank >= 14) {
|
||||
bank = 14 | address.bit(6);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -18,24 +18,72 @@ auto Cartridge::connect() -> void {
|
||||
information.title = pak->attribute("title");
|
||||
information.region = pak->attribute("region");
|
||||
information.board = pak->attribute("board");
|
||||
information.phosphor = pak->attribute("phosphor").boolean();
|
||||
if(auto fp = pak->read("program.rom")) {
|
||||
information.sha256 = Hash::SHA256({fp->data(), fp->size()}).digest();
|
||||
}
|
||||
|
||||
if(information.board == "Linear") board = new Board::Linear{*this};
|
||||
if(information.board == "Atari8k") board = new Board::Atari8k{*this};
|
||||
if(information.board == "Atari16k") board = new Board::Atari16k{*this};
|
||||
if(information.board == "Atari32k") board = new Board::Atari32k{*this};
|
||||
if(information.board == "Commavid") board = new Board::Commavid{*this};
|
||||
if(information.board == "ParkerBros8k") board = new Board::ParkerBros{*this};
|
||||
if(information.board == "Tigervision") board = new Board::Tigervision{*this};
|
||||
if(information.board == "Linear") board = std::make_unique<Board::Linear>(*this);
|
||||
if(information.board == "Activision8k") board = std::make_unique<Board::Activision8k>(*this);
|
||||
if(information.board == "Atari8k") board = std::make_unique<Board::Atari8k>(*this);
|
||||
if(information.board == "Atari8kSC") board = std::make_unique<Board::Atari8k>(*this, true);
|
||||
if(information.board == "Atari16k") board = std::make_unique<Board::Atari16k>(*this);
|
||||
if(information.board == "Atari16kSC") board = std::make_unique<Board::Atari16k>(*this, true);
|
||||
if(information.board == "Atari32k") board = std::make_unique<Board::Atari32k>(*this);
|
||||
if(information.board == "Atari32kSC") board = std::make_unique<Board::Atari32k>(*this, true);
|
||||
if(information.board == "Commavid") board = std::make_unique<Board::Commavid>(*this);
|
||||
if(information.board == "ParkerBros8k") board = std::make_unique<Board::ParkerBros>(*this);
|
||||
if(information.board == "ParkerBros03E0") board = std::make_unique<Board::ParkerBros03E0>(*this);
|
||||
if(information.board == "JVP") board = std::make_unique<Board::JVP>(*this);
|
||||
if(information.board == "Tigervision") board = std::make_unique<Board::Tigervision>(*this);
|
||||
if(information.board == "UA8k") board = std::make_unique<Board::UA8k>(*this);
|
||||
if(information.board == "UASW") board = std::make_unique<Board::UA8k>(*this, true);
|
||||
if(information.board == "CbsRamPlus") board = std::make_unique<Board::CbsRamPlus>(*this);
|
||||
if(information.board == "MNetwork") board = std::make_unique<Board::MNetwork>(*this);
|
||||
if(information.board == "AmigaFC") board = std::make_unique<Board::AmigaFC>(*this);
|
||||
if(information.board == "Wickstead") board = std::make_unique<Board::Wickstead>(*this);
|
||||
if(information.board == "Jane") board = std::make_unique<Board::Jane>(*this);
|
||||
if(information.board == "DPC") board = std::make_unique<Board::DPC>(*this);
|
||||
if(information.board == "MegaBoy") board = std::make_unique<Board::MegaBoy>(*this);
|
||||
if(information.board == "Atari32In1") board = std::make_unique<Board::Atari32In1>(*this);
|
||||
|
||||
if(!board) board = new Board::Interface{*this};
|
||||
//Homebrew
|
||||
if(information.board == "DPC+") board = std::make_unique<Board::DPCPlus>(*this);
|
||||
if(information.board == "CDF") board = std::make_unique<Board::CDF>(*this);
|
||||
if(information.board == "BUS") board = std::make_unique<Board::BUS>(*this);
|
||||
if(information.board == "Chetiry") board = std::make_unique<Board::Chetiry>(*this);
|
||||
if(information.board == "FA2") board = std::make_unique<Board::FA2>(*this);
|
||||
if(information.board == "ELF") board = std::make_unique<Board::ELFCartridge>(*this);
|
||||
if(information.board == "MovieCart") board = std::make_unique<Board::MovieCart>(*this);
|
||||
if(information.board == "4KSC") board = std::make_unique<Board::CPUWiz4KSC>(*this);
|
||||
if(information.board == "3E") board = std::make_unique<Board::ThreeE>(*this);
|
||||
if(information.board == "3EX") board = std::make_unique<Board::ThreeEX>(*this);
|
||||
if(information.board == "3E+") board = std::make_unique<Board::ThreeEPlus>(*this);
|
||||
if(information.board == "Enhanced3F") board = std::make_unique<Board::Enhanced3F>(*this);
|
||||
if(information.board == "4A50") board = std::make_unique<Board::FourA50>(*this);
|
||||
if(information.board == "EF") board = std::make_unique<Board::EF>(*this);
|
||||
if(information.board == "EFF") board = std::make_unique<Board::EFF>(*this);
|
||||
if(information.board == "DF") board = std::make_unique<Board::DF>(*this);
|
||||
if(information.board == "BF") board = std::make_unique<Board::BF>(*this);
|
||||
if(information.board == "EFSC") board = std::make_unique<Board::EFSC>(*this);
|
||||
if(information.board == "DFSC") board = std::make_unique<Board::DFSC>(*this);
|
||||
if(information.board == "BFSC") board = std::make_unique<Board::BFSC>(*this);
|
||||
if(information.board == "MDM") board = std::make_unique<Board::MDM>(*this);
|
||||
if(information.board == "X07") board = std::make_unique<Board::X07>(*this);
|
||||
if(information.board == "EconoBanking") board = std::make_unique<Board::EconoBanking>(*this);
|
||||
if(information.board == "Superbanking") board = std::make_unique<Board::Superbanking>(*this);
|
||||
|
||||
if(!board) board = std::make_unique<Board::Interface>(*this);
|
||||
board->pak = pak;
|
||||
board->load();
|
||||
power();
|
||||
video.setPhosphor(information.phosphor);
|
||||
power(false);
|
||||
}
|
||||
|
||||
auto Cartridge::disconnect() -> void {
|
||||
if(!node || !board) return;
|
||||
board->unload();
|
||||
video.setPhosphor(false);
|
||||
board->pak.reset();
|
||||
board.reset();
|
||||
node.reset();
|
||||
@@ -46,21 +94,44 @@ auto Cartridge::save() -> void {
|
||||
if(board) board->save();
|
||||
}
|
||||
|
||||
auto Cartridge::power() -> void {
|
||||
if(board) board->power();
|
||||
auto Cartridge::power(bool reset) -> void {
|
||||
if(board) board->power(reset);
|
||||
}
|
||||
|
||||
auto Cartridge::read(n16 address) -> n8 {
|
||||
if(!node) return 0xff;
|
||||
if(board) return board->read(address);
|
||||
|
||||
return 0xff;
|
||||
auto Cartridge::read(n16 address, n8 data) -> n8 {
|
||||
if(!node) return data;
|
||||
if(board) return board->read(address, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto Cartridge::write(n16 address, n8 data) -> bool {
|
||||
if(!node) return false;
|
||||
auto Cartridge::write(n16 address, n8 data) -> n8 {
|
||||
if(!node) return data;
|
||||
if(board) return board->write(address, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
auto Cartridge::armInvocation() const -> Harmony::Invocation {
|
||||
if(board) return board->armInvocation();
|
||||
return {};
|
||||
}
|
||||
|
||||
auto Cartridge::readARM(u32 mode, n32 address, n32& data) -> Harmony::Access {
|
||||
if(board) return board->readARM(mode, address, data);
|
||||
return Harmony::Access::Unmapped;
|
||||
}
|
||||
|
||||
auto Cartridge::writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access {
|
||||
if(board) return board->writeARM(mode, address, data);
|
||||
return Harmony::Access::Unmapped;
|
||||
}
|
||||
|
||||
auto Cartridge::trapARM(u32 address, n32& value, n32 argument) -> bool {
|
||||
if(board) return board->trapARM(address, value, argument);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto Cartridge::stepARM(u32 clocks) -> void {
|
||||
if(board) board->stepARM(clocks);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ struct Cartridge {
|
||||
|
||||
auto title() const -> string { return information.title; }
|
||||
auto region() const -> string { return information.region; }
|
||||
auto sha256() const -> string { return information.sha256; }
|
||||
|
||||
//cartridge.cpp
|
||||
auto allocate(Node::Port) -> Node::Peripheral;
|
||||
@@ -14,21 +15,29 @@ struct Cartridge {
|
||||
auto disconnect() -> void;
|
||||
|
||||
auto save() -> void;
|
||||
auto power() -> void;
|
||||
auto power(bool reset) -> void;
|
||||
|
||||
auto read(n16 address) -> n8;
|
||||
auto write(n16 address, n8 data) -> bool;
|
||||
auto read(n16 address, n8 data) -> n8;
|
||||
auto write(n16 address, n8 data) -> n8;
|
||||
|
||||
auto armInvocation() const -> Harmony::Invocation;
|
||||
auto readARM(u32 mode, n32 address, n32& data) -> Harmony::Access;
|
||||
auto writeARM(u32 mode, n32 address, n32 data) -> Harmony::Access;
|
||||
auto trapARM(u32 address, n32& value, n32 argument) -> bool;
|
||||
auto stepARM(u32 clocks) -> void;
|
||||
|
||||
//serialization.cpp
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
unique_pointer<Board::Interface> board;
|
||||
std::unique_ptr<Board::Interface> board;
|
||||
|
||||
//private:
|
||||
struct Information {
|
||||
string title;
|
||||
string region;
|
||||
string board;
|
||||
string sha256;
|
||||
bool phosphor;
|
||||
} information;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
AmigaMouse::AmigaMouse(Node::Port parent)
|
||||
: RelativePointingDevice(parent, "Amiga Mouse", 4) {
|
||||
}
|
||||
|
||||
auto AmigaMouse::encode(u8 xPhase, u8 yPhase, bool, bool) const -> u8 {
|
||||
static constexpr u8 horizontal[4] = {0b0000, 0b1000, 0b1010, 0b0010};
|
||||
static constexpr u8 vertical[4] = {0b0000, 0b0100, 0b0101, 0b0001};
|
||||
return horizontal[xPhase & 3] | vertical[yPhase & 3];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
struct AmigaMouse : RelativePointingDevice {
|
||||
AmigaMouse(Node::Port);
|
||||
|
||||
protected:
|
||||
auto encode(u8 xPhase, u8 yPhase, bool left, bool down) const -> u8 override;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
AtariMouse::AtariMouse(Node::Port parent)
|
||||
: RelativePointingDevice(parent, "Atari Mouse", 4) {
|
||||
}
|
||||
|
||||
auto AtariMouse::encode(u8 xPhase, u8 yPhase, bool, bool) const -> u8 {
|
||||
static constexpr u8 horizontal[4] = {0b0000, 0b0001, 0b0011, 0b0010};
|
||||
static constexpr u8 vertical[4] = {0b0000, 0b0100, 0b1100, 0b1000};
|
||||
return horizontal[xPhase & 3] | vertical[yPhase & 3];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
struct AtariMouse : RelativePointingDevice {
|
||||
AtariMouse(Node::Port);
|
||||
|
||||
protected:
|
||||
auto encode(u8 xPhase, u8 yPhase, bool left, bool down) const -> u8 override;
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
AtariVox::AtariVox(Node::Port parent) : SaveKey(parent, "AtariVox", "atarivox.eeprom") {
|
||||
stream = node->append<Node::Audio::Stream>("AtariVox");
|
||||
stream->setChannels(1);
|
||||
stream->setFrequency(SpeakJet::Frequency);
|
||||
|
||||
speakjet.reset();
|
||||
serialBits = 0;
|
||||
serialData = 0;
|
||||
serialValid = 1;
|
||||
serialCycle = 0;
|
||||
Thread::create(SpeakJet::Frequency, std::bind_front(&AtariVox::main, this));
|
||||
}
|
||||
|
||||
AtariVox::~AtariVox() {
|
||||
Thread::destroy();
|
||||
stream.reset();
|
||||
}
|
||||
|
||||
auto AtariVox::main() -> void {
|
||||
stream->frame(speakjet.clock());
|
||||
Thread::step(1);
|
||||
Thread::synchronize(cpu);
|
||||
}
|
||||
|
||||
auto AtariVox::power(bool reset) -> void {
|
||||
SaveKey::power(reset);
|
||||
speakjet.reset();
|
||||
serialBits = 0;
|
||||
serialData = 0;
|
||||
serialValid = 1;
|
||||
serialCycle = 0;
|
||||
Thread::create(SpeakJet::Frequency, std::bind_front(&AtariVox::main, this));
|
||||
}
|
||||
|
||||
auto AtariVox::read() -> n8 {
|
||||
n8 data = SaveKey::read();
|
||||
data.bit(1) = speakjet.ready();
|
||||
return data;
|
||||
}
|
||||
|
||||
auto AtariVox::write(n8 data) -> void {
|
||||
SaveKey::write(data);
|
||||
receiveSerial(data.bit(0));
|
||||
}
|
||||
|
||||
auto AtariVox::serialize(serializer& s) -> void {
|
||||
SaveKey::serialize(s);
|
||||
Thread::serialize(s);
|
||||
speakjet.serialize(s);
|
||||
s(serialBits);
|
||||
s(serialData);
|
||||
s(serialValid);
|
||||
s(serialCycle);
|
||||
}
|
||||
|
||||
auto AtariVox::receiveSerial(bool level) -> void {
|
||||
if(!cpu.scalar()) return;
|
||||
//The CPU Thread advances three color clocks per 6507 cycle. Stella's serial
|
||||
//thresholds are expressed in CPU cycles, so compare in that same domain.
|
||||
auto cycle = cpu.clock() / cpu.scalar() / 3;
|
||||
|
||||
if(serialBits && (cycle < serialCycle || cycle > serialCycle + 1000)) {
|
||||
serialBits = 0;
|
||||
serialData = 0;
|
||||
serialValid = 1;
|
||||
}
|
||||
|
||||
if(!serialBits && level) {
|
||||
serialCycle = cycle;
|
||||
return;
|
||||
}
|
||||
|
||||
if(serialBits && cycle >= serialCycle && cycle < serialCycle + 62) {
|
||||
serialCycle = cycle;
|
||||
return;
|
||||
}
|
||||
|
||||
if(serialBits == 0) {
|
||||
serialValid = !level;
|
||||
} else if(serialBits <= 8) {
|
||||
serialData.bit(serialBits - 1) = level;
|
||||
} else {
|
||||
serialValid &= level;
|
||||
}
|
||||
|
||||
serialCycle = cycle;
|
||||
if(++serialBits < 10) return;
|
||||
if(serialValid) speakjet.enqueue(serialData);
|
||||
serialBits = 0;
|
||||
serialData = 0;
|
||||
serialValid = 1;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "speakjet.hpp"
|
||||
|
||||
struct AtariVox : SaveKey, Thread {
|
||||
AtariVox(Node::Port);
|
||||
~AtariVox();
|
||||
|
||||
auto main() -> void;
|
||||
auto power(bool reset) -> void override;
|
||||
auto read() -> n8 override;
|
||||
auto write(n8 data) -> void override;
|
||||
auto serialize(serializer&) -> void override;
|
||||
|
||||
private:
|
||||
auto receiveSerial(bool level) -> void;
|
||||
|
||||
Node::Audio::Stream stream;
|
||||
SpeakJet speakjet;
|
||||
|
||||
n4 serialBits;
|
||||
n8 serialData;
|
||||
n1 serialValid;
|
||||
u64 serialCycle;
|
||||
};
|
||||
@@ -0,0 +1,347 @@
|
||||
namespace {
|
||||
|
||||
constexpr u16 AllophoneDuration[] = {
|
||||
70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
|
||||
70, 70, 70, 70, 70, 70, 70, 70, 70, 200, 200, 190,
|
||||
200, 185, 165, 200, 225, 185, 170, 140, 180, 170, 170, 200,
|
||||
131, 70, 70, 70, 70, 70, 45, 45, 10, 10, 45, 45,
|
||||
10, 10, 55, 55, 55, 55, 70, 70, 70, 70, 70, 40,
|
||||
40, 50, 40, 50, 70, 170, 55, 55, 55, 45, 99, 99,
|
||||
};
|
||||
|
||||
constexpr u16 EffectDuration[] = {
|
||||
80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
|
||||
300, 101, 102, 540, 530, 500, 135, 600, 300, 250,
|
||||
200, 270, 280, 260, 300, 100, 104, 100, 270, 262,
|
||||
160, 300, 182, 120, 175, 350, 160, 260, 95, 75,
|
||||
95, 95, 95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
|
||||
125, 250, 530,
|
||||
};
|
||||
|
||||
constexpr u16 PauseDuration[] = {0, 100, 200, 700, 30, 60, 90};
|
||||
|
||||
static_assert(std::size(AllophoneDuration) == 72);
|
||||
static_assert(std::size(EffectDuration) == 55);
|
||||
|
||||
}
|
||||
|
||||
auto SpeakJet::reset() -> void {
|
||||
std::fill(std::begin(fifo), std::end(fifo), 0);
|
||||
fifoRead = 0;
|
||||
fifoWrite = 0;
|
||||
fifoCount = 0;
|
||||
parameterPending = 0;
|
||||
pendingCommand = 0;
|
||||
waiting = 0;
|
||||
volume = 96;
|
||||
speed = 114;
|
||||
pitch = 88;
|
||||
bend = 5;
|
||||
portControl = 7;
|
||||
portOutput = 0;
|
||||
repeatRequest = 0;
|
||||
repeatCode = 0;
|
||||
repeatRemaining = 0;
|
||||
nextDurationScale = 1.0;
|
||||
nextPitchScale = 1.0;
|
||||
soundCode = 0;
|
||||
soundClass = SoundClass::None;
|
||||
soundSamples = 0;
|
||||
soundRemaining = 0;
|
||||
soundPitchScale = 1.0;
|
||||
std::fill(std::begin(phase), std::end(phase), 0.0);
|
||||
for(auto& stage : filter) std::fill(std::begin(stage), std::end(stage), 0.0);
|
||||
noiseLfsr = 0x4a3d;
|
||||
}
|
||||
|
||||
auto SpeakJet::enqueue(u8 data) -> bool {
|
||||
if(fifoCount == 64) return false;
|
||||
fifo[fifoWrite++] = data;
|
||||
fifoCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto SpeakJet::clock() -> f64 {
|
||||
if(!soundRemaining && !waiting) processCommands();
|
||||
if(!soundRemaining) return 0.0;
|
||||
|
||||
auto sample = synthesize();
|
||||
if(!--soundRemaining) finishSound();
|
||||
return sample;
|
||||
}
|
||||
|
||||
auto SpeakJet::serialize(serializer& s) -> void {
|
||||
for(auto& byte : fifo) s(byte);
|
||||
s(fifoRead);
|
||||
s(fifoWrite);
|
||||
s(fifoCount);
|
||||
s(parameterPending);
|
||||
s(pendingCommand);
|
||||
s(waiting);
|
||||
s(volume);
|
||||
s(speed);
|
||||
s(pitch);
|
||||
s(bend);
|
||||
s(portControl);
|
||||
s(portOutput);
|
||||
s(repeatRequest);
|
||||
s(repeatCode);
|
||||
s(repeatRemaining);
|
||||
s(nextDurationScale);
|
||||
s(nextPitchScale);
|
||||
s(soundCode);
|
||||
s(soundClass);
|
||||
s(soundSamples);
|
||||
s(soundRemaining);
|
||||
s(soundPitchScale);
|
||||
for(auto& value : phase) s(value);
|
||||
for(auto& stage : filter) for(auto& value : stage) s(value);
|
||||
s(noiseLfsr);
|
||||
}
|
||||
|
||||
auto SpeakJet::dequeue() -> u8 {
|
||||
if(!fifoCount) return 0;
|
||||
auto data = fifo[fifoRead++];
|
||||
fifoCount--;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto SpeakJet::processCommands() -> void {
|
||||
for(u32 commands : range(64)) {
|
||||
if(parameterPending) {
|
||||
if(!fifoCount) return;
|
||||
parameter(pendingCommand, dequeue());
|
||||
parameterPending = 0;
|
||||
if(soundRemaining || waiting) return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(repeatRemaining) {
|
||||
execute(repeatCode, true);
|
||||
if(soundRemaining || waiting) return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!fifoCount) return;
|
||||
execute(dequeue());
|
||||
if(soundRemaining || waiting) return;
|
||||
}
|
||||
}
|
||||
|
||||
auto SpeakJet::execute(u8 code, bool repeated) -> void {
|
||||
if(repeated) repeatRemaining--;
|
||||
|
||||
if(code <= 6) {
|
||||
startSound(code);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(code) {
|
||||
case 7: nextDurationScale = 0.5; return;
|
||||
case 8: nextDurationScale = 1.5; return;
|
||||
case 14: nextPitchScale = 1.08; return;
|
||||
case 15: nextPitchScale = 0.92; return;
|
||||
case 16: waiting = 1; return;
|
||||
case 20: case 21: case 22: case 23: case 24: case 25: case 26: case 28: case 29: case 30:
|
||||
pendingCommand = code;
|
||||
parameterPending = 1;
|
||||
return;
|
||||
case 31:
|
||||
volume = 96;
|
||||
speed = 114;
|
||||
pitch = 88;
|
||||
bend = 5;
|
||||
nextDurationScale = 1.0;
|
||||
nextPitchScale = 1.0;
|
||||
return;
|
||||
case 255:
|
||||
repeatRequest = 0;
|
||||
repeatRemaining = 0;
|
||||
nextDurationScale = 1.0;
|
||||
nextPitchScale = 1.0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(code < 128) return;
|
||||
if(!repeated && repeatRequest) {
|
||||
repeatCode = code;
|
||||
repeatRemaining = repeatRequest;
|
||||
repeatRequest = 0;
|
||||
}
|
||||
startSound(code);
|
||||
}
|
||||
|
||||
auto SpeakJet::parameter(u8 command, u8 value) -> void {
|
||||
switch(command) {
|
||||
case 20: volume = min(value, (u8)127); break;
|
||||
case 21: speed = min(value, (u8)127); break;
|
||||
case 22: pitch = value; break;
|
||||
case 23: bend = min(value, (u8)15); break;
|
||||
case 24: portControl = value & 7; break;
|
||||
case 25: portOutput = value & 7; break;
|
||||
case 26: repeatRequest = value; break;
|
||||
case 28: break; //Phrase EEPROM execution is outside the behavioral model.
|
||||
case 29: break; //Phrase EEPROM execution is outside the behavioral model.
|
||||
case 30:
|
||||
soundCode = command;
|
||||
soundClass = SoundClass::Pause;
|
||||
auto samples = value * Frequency / 100.0;
|
||||
soundSamples = soundRemaining = samples > 0.0 ? max(1u, (u32)(samples + 0.5)) : 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto SpeakJet::startSound(u8 code) -> void {
|
||||
soundCode = code;
|
||||
soundClass = classify(code);
|
||||
auto milliseconds = duration(code);
|
||||
auto scaled = milliseconds * Frequency / 1000.0;
|
||||
scaled *= 114.0 / max(1.0, (f64)speed);
|
||||
scaled *= nextDurationScale;
|
||||
soundSamples = soundRemaining = scaled > 0.0 ? max(1u, (u32)(scaled + 0.5)) : 0;
|
||||
soundPitchScale = nextPitchScale;
|
||||
nextDurationScale = 1.0;
|
||||
nextPitchScale = 1.0;
|
||||
std::fill(std::begin(phase), std::end(phase), 0.0);
|
||||
for(auto& stage : filter) std::fill(std::begin(stage), std::end(stage), 0.0);
|
||||
}
|
||||
|
||||
auto SpeakJet::finishSound() -> void {
|
||||
soundClass = SoundClass::None;
|
||||
soundSamples = 0;
|
||||
}
|
||||
|
||||
auto SpeakJet::synthesize() -> f64 {
|
||||
if(soundClass == SoundClass::Pause) return 0.0;
|
||||
auto progress = soundSamples ? 1.0 - (f64)soundRemaining / soundSamples : 1.0;
|
||||
auto attack = min(1.0, progress * 12.0);
|
||||
auto release = min(1.0, (1.0 - progress) * 10.0);
|
||||
auto envelope = attack * release;
|
||||
auto sample = soundCode < 200 ? synthesizeSpeech(progress) : synthesizeEffect(progress);
|
||||
sample *= envelope * ((f64)volume / 127.0);
|
||||
return max(-1.0, min(1.0, sample));
|
||||
}
|
||||
|
||||
auto SpeakJet::synthesizeSpeech(f64 progress) -> f64 {
|
||||
//The public MSA tables specify identities and durations, not oscillator trajectories.
|
||||
//Use a deterministic phonetic-class model rather than importing a third-party voice table.
|
||||
auto identity = soundCode - 128;
|
||||
auto voiced = soundCode <= 181;
|
||||
auto fundamental = (f64)pitch * soundPitchScale;
|
||||
auto glottal = fundamental ? oscillator(0, fundamental) * 2.0 - 1.0 : 0.0;
|
||||
auto source = voiced ? glottal : 0.0;
|
||||
auto breath = noise();
|
||||
|
||||
if(soundClass == SoundClass::Fricative) source = voiced ? 0.4 * glottal + 0.75 * breath : breath;
|
||||
if(soundClass == SoundClass::Affricate) source = 0.35 * glottal + 0.85 * breath;
|
||||
if(soundClass == SoundClass::Stop) {
|
||||
if(progress < 0.45) source = 0.0;
|
||||
else source = (voiced ? 0.25 * glottal : 0.0) + breath * (1.0 - progress);
|
||||
}
|
||||
if(soundClass == SoundClass::Nasal) source = 0.75 * glottal + 0.1 * breath;
|
||||
|
||||
auto bendScale = 0.75 + (f64)bend * 0.05;
|
||||
auto formant1 = (330.0 + (identity * 73) % 520) * bendScale;
|
||||
auto formant2 = (850.0 + (identity * 97) % 1300) * bendScale;
|
||||
auto formant3 = (2100.0 + (identity * 53) % 1050) * bendScale;
|
||||
if(soundClass == SoundClass::Nasal) {
|
||||
formant1 *= 0.72;
|
||||
formant2 *= 0.82;
|
||||
}
|
||||
if(soundClass == SoundClass::Resonate) formant2 *= 0.88;
|
||||
|
||||
auto low = resonator(0, source, formant1, 110.0);
|
||||
auto middle = resonator(1, source, formant2, 170.0);
|
||||
auto high = resonator(2, source, formant3, 260.0);
|
||||
return 0.95 * low + 0.65 * middle + 0.4 * high;
|
||||
}
|
||||
|
||||
auto SpeakJet::synthesizeEffect(f64 progress) -> f64 {
|
||||
auto index = soundCode % 10;
|
||||
|
||||
switch(soundClass) {
|
||||
case SoundClass::Robot: {
|
||||
auto carrier = oscillator(0, 90.0 + index * 31.0) < 0.5 ? -1.0 : 1.0;
|
||||
auto modulator = oscillator(1, 8.0 + index * 2.0) * 2.0 - 1.0;
|
||||
return carrier * (0.45 + 0.35 * modulator);
|
||||
}
|
||||
case SoundClass::Alarm: {
|
||||
auto sweep = 250.0 + index * 45.0 + 900.0 * progress;
|
||||
return 0.65 * sin(2.0 * Math::Pi * oscillator(0, sweep))
|
||||
+ 0.25 * sin(2.0 * Math::Pi * oscillator(1, sweep * 1.51));
|
||||
}
|
||||
case SoundClass::Beep:
|
||||
return 0.8 * sin(2.0 * Math::Pi * oscillator(0, 420.0 + index * 145.0));
|
||||
case SoundClass::Biological: {
|
||||
auto chirp = 130.0 + index * 37.0 + sin(progress * Math::Pi * 6.0) * (90.0 + index * 9.0);
|
||||
return 0.6 * sin(2.0 * Math::Pi * oscillator(0, chirp)) + 0.2 * noise();
|
||||
}
|
||||
case SoundClass::DTMF: {
|
||||
static constexpr u16 low[] = {941, 697, 697, 697, 770, 770, 770, 852, 852, 852, 941, 941};
|
||||
static constexpr u16 high[] = {1336, 1209, 1336, 1477, 1209, 1336, 1477, 1209, 1336, 1477, 1209, 1477};
|
||||
auto digit = soundCode - 240;
|
||||
return 0.45 * sin(2.0 * Math::Pi * oscillator(0, low[digit]))
|
||||
+ 0.45 * sin(2.0 * Math::Pi * oscillator(1, high[digit]));
|
||||
}
|
||||
case SoundClass::Sonar: {
|
||||
auto frequency = 1500.0 - 900.0 * progress;
|
||||
return sin(2.0 * Math::Pi * oscillator(0, frequency)) * (1.0 - progress);
|
||||
}
|
||||
case SoundClass::Pistol:
|
||||
return noise() * (1.0 - progress) * (1.0 - progress);
|
||||
case SoundClass::Wow: {
|
||||
auto frequency = 180.0 + 780.0 * sin(progress * Math::Pi);
|
||||
return 0.7 * sin(2.0 * Math::Pi * oscillator(0, frequency));
|
||||
}
|
||||
default: return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
auto SpeakJet::resonator(u32 index, f64 input, f64 frequency, f64 bandwidth) -> f64 {
|
||||
frequency = min(frequency, 3999.0);
|
||||
auto radius = exp(-Math::Pi * bandwidth / Frequency);
|
||||
auto output = (1.0 - radius) * input
|
||||
+ 2.0 * radius * cos(2.0 * Math::Pi * frequency / Frequency) * filter[index][0]
|
||||
- radius * radius * filter[index][1];
|
||||
filter[index][1] = filter[index][0];
|
||||
filter[index][0] = output;
|
||||
return output;
|
||||
}
|
||||
|
||||
auto SpeakJet::oscillator(u32 index, f64 frequency) -> f64 {
|
||||
phase[index] += min(frequency, 3999.0) / Frequency;
|
||||
if(phase[index] >= 1.0) phase[index] -= 1.0;
|
||||
return phase[index];
|
||||
}
|
||||
|
||||
auto SpeakJet::noise() -> f64 {
|
||||
auto feedback = (noiseLfsr ^ (noiseLfsr >> 2) ^ (noiseLfsr >> 3) ^ (noiseLfsr >> 5)) & 1;
|
||||
noiseLfsr = (noiseLfsr >> 1) | feedback << 15;
|
||||
return (noiseLfsr & 1) ? 1.0 : -1.0;
|
||||
}
|
||||
|
||||
auto SpeakJet::duration(u8 code) -> u32 {
|
||||
if(code <= 6) return PauseDuration[code];
|
||||
if(code >= 128 && code <= 199) return AllophoneDuration[code - 128];
|
||||
if(code >= 200 && code <= 254) return EffectDuration[code - 200];
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto SpeakJet::classify(u8 code) -> SoundClass {
|
||||
if(code <= 6) return SoundClass::Pause;
|
||||
if(code >= 128 && (code <= 139 || code >= 149 && code <= 164)) return SoundClass::Vowel;
|
||||
if(code >= 140 && code <= 144) return SoundClass::Nasal;
|
||||
if(code >= 145 && code <= 148) return SoundClass::Resonate;
|
||||
if(code == 165 || code == 182) return SoundClass::Affricate;
|
||||
if((code >= 166 && code <= 169) || (code >= 183 && code <= 190)) return SoundClass::Fricative;
|
||||
if((code >= 170 && code <= 181) || (code >= 191 && code <= 199)) return SoundClass::Stop;
|
||||
if(code >= 200 && code <= 209) return SoundClass::Robot;
|
||||
if(code >= 210 && code <= 219) return SoundClass::Alarm;
|
||||
if(code >= 220 && code <= 229) return SoundClass::Beep;
|
||||
if(code >= 230 && code <= 239) return SoundClass::Biological;
|
||||
if(code >= 240 && code <= 251) return SoundClass::DTMF;
|
||||
if(code == 252) return SoundClass::Sonar;
|
||||
if(code == 253) return SoundClass::Pistol;
|
||||
if(code == 254) return SoundClass::Wow;
|
||||
return SoundClass::None;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
struct SpeakJet {
|
||||
static constexpr f64 Frequency = 8192.0;
|
||||
|
||||
enum class SoundClass : u8 {
|
||||
None,
|
||||
Vowel,
|
||||
Nasal,
|
||||
Resonate,
|
||||
Affricate,
|
||||
Fricative,
|
||||
Stop,
|
||||
Robot,
|
||||
Alarm,
|
||||
Beep,
|
||||
Biological,
|
||||
DTMF,
|
||||
Sonar,
|
||||
Pistol,
|
||||
Wow,
|
||||
Pause,
|
||||
};
|
||||
|
||||
auto reset() -> void;
|
||||
auto enqueue(u8 data) -> bool;
|
||||
auto ready() const -> bool { return fifoCount < 64; }
|
||||
auto buffered() const -> u32 { return fifoCount; }
|
||||
auto speaking() const -> bool { return soundRemaining; }
|
||||
auto clock() -> f64;
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
static auto duration(u8 code) -> u32;
|
||||
static auto classify(u8 code) -> SoundClass;
|
||||
|
||||
private:
|
||||
auto dequeue() -> u8;
|
||||
auto processCommands() -> void;
|
||||
auto execute(u8 code, bool repeated = false) -> void;
|
||||
auto parameter(u8 command, u8 value) -> void;
|
||||
auto startSound(u8 code) -> void;
|
||||
auto finishSound() -> void;
|
||||
auto synthesize() -> f64;
|
||||
auto synthesizeSpeech(f64 progress) -> f64;
|
||||
auto synthesizeEffect(f64 progress) -> f64;
|
||||
auto resonator(u32 index, f64 input, f64 frequency, f64 bandwidth) -> f64;
|
||||
auto oscillator(u32 index, f64 frequency) -> f64;
|
||||
auto noise() -> f64;
|
||||
|
||||
u8 fifo[64] = {};
|
||||
n6 fifoRead;
|
||||
n6 fifoWrite;
|
||||
n7 fifoCount;
|
||||
|
||||
n1 parameterPending;
|
||||
u8 pendingCommand;
|
||||
n1 waiting;
|
||||
u8 volume;
|
||||
u8 speed;
|
||||
u8 pitch;
|
||||
u8 bend;
|
||||
n3 portControl;
|
||||
n3 portOutput;
|
||||
u8 repeatRequest;
|
||||
u8 repeatCode;
|
||||
u8 repeatRemaining;
|
||||
f64 nextDurationScale;
|
||||
f64 nextPitchScale;
|
||||
|
||||
u8 soundCode;
|
||||
SoundClass soundClass;
|
||||
u32 soundSamples;
|
||||
u32 soundRemaining;
|
||||
f64 soundPitchScale;
|
||||
f64 phase[5] = {};
|
||||
f64 filter[3][2] = {};
|
||||
u16 noiseLfsr;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
BoosterGrip::BoosterGrip(Node::Port parent) : Gamepad(parent, "Booster Grip") {
|
||||
booster = node->append<Node::Input::Button>("Booster");
|
||||
trigger = node->append<Node::Input::Button>("Trigger");
|
||||
}
|
||||
|
||||
auto BoosterGrip::readAnalog(n1 index) -> AnalogConnection {
|
||||
auto& button = index ? booster : trigger;
|
||||
platform->input(button);
|
||||
return button->value() ? AnalogConnection::vcc() : AnalogConnection::disconnected();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
struct BoosterGrip : Gamepad {
|
||||
Node::Input::Button booster;
|
||||
Node::Input::Button trigger;
|
||||
|
||||
BoosterGrip(Node::Port);
|
||||
|
||||
auto readAnalog(n1 index) -> AnalogConnection override;
|
||||
};
|
||||
@@ -1,8 +1,27 @@
|
||||
#include <a26/a26.hpp>
|
||||
#include <ares/resource/resource.hpp>
|
||||
|
||||
namespace ares::Atari2600 {
|
||||
|
||||
#include "port.cpp"
|
||||
#include "gamepad/gamepad.cpp"
|
||||
#include "booster-grip/booster-grip.cpp"
|
||||
#include "sega-genesis/sega-genesis.cpp"
|
||||
#include "joy2b-plus/joy2b-plus.cpp"
|
||||
#include "paddles/paddles.cpp"
|
||||
#include "driving/driving.cpp"
|
||||
#include "keyboard/keyboard.cpp"
|
||||
#include "relative-pointing/relative-pointing.cpp"
|
||||
#include "trak-ball/trak-ball.cpp"
|
||||
#include "atari-mouse/atari-mouse.cpp"
|
||||
#include "amiga-mouse/amiga-mouse.cpp"
|
||||
#include "xg1-light-gun/xg1-light-gun.cpp"
|
||||
#include "mindlink/mindlink.cpp"
|
||||
#include "savekey/savekey.cpp"
|
||||
#include "atarivox/speakjet.cpp"
|
||||
#include "atarivox/atarivox.cpp"
|
||||
#include "kidvid/audio.cpp"
|
||||
#include "kidvid/kidvid.cpp"
|
||||
#include "quadtari/quadtari.cpp"
|
||||
|
||||
}
|
||||
@@ -1,11 +1,47 @@
|
||||
struct Controller {
|
||||
Node::Peripheral node;
|
||||
|
||||
struct AnalogConnection {
|
||||
enum class Type : u32 { Disconnected, Ground, Vcc };
|
||||
|
||||
static auto disconnected() -> AnalogConnection { return {}; }
|
||||
static auto ground(u32 resistance = 0) -> AnalogConnection { return {Type::Ground, resistance}; }
|
||||
static auto vcc(u32 resistance = 0) -> AnalogConnection { return {Type::Vcc, resistance}; }
|
||||
|
||||
Type type = Type::Disconnected;
|
||||
u32 resistance = 0;
|
||||
};
|
||||
|
||||
virtual ~Controller() = default;
|
||||
|
||||
virtual auto save() -> void {}
|
||||
virtual auto power(bool reset) -> void {}
|
||||
virtual auto poll() -> void {}
|
||||
virtual auto frame() -> void {}
|
||||
virtual auto clock() -> void {}
|
||||
virtual auto vblank(n1 dumped) -> void {}
|
||||
virtual auto read() -> n8 { return 0xff; }
|
||||
virtual auto write(n8 data) -> void {}
|
||||
virtual auto controlWrite(n8 data) -> void {}
|
||||
virtual auto readAnalog(n1 index) -> AnalogConnection { return AnalogConnection::disconnected(); }
|
||||
virtual auto serialize(serializer&) -> void {}
|
||||
};
|
||||
|
||||
#include "port.hpp"
|
||||
#include "gamepad/gamepad.hpp"
|
||||
#include "booster-grip/booster-grip.hpp"
|
||||
#include "sega-genesis/sega-genesis.hpp"
|
||||
#include "joy2b-plus/joy2b-plus.hpp"
|
||||
#include "paddles/paddles.hpp"
|
||||
#include "driving/driving.hpp"
|
||||
#include "keyboard/keyboard.hpp"
|
||||
#include "relative-pointing/relative-pointing.hpp"
|
||||
#include "trak-ball/trak-ball.hpp"
|
||||
#include "atari-mouse/atari-mouse.hpp"
|
||||
#include "amiga-mouse/amiga-mouse.hpp"
|
||||
#include "xg1-light-gun/xg1-light-gun.hpp"
|
||||
#include "mindlink/mindlink.hpp"
|
||||
#include "savekey/savekey.hpp"
|
||||
#include "atarivox/atarivox.hpp"
|
||||
#include "kidvid/kidvid.hpp"
|
||||
#include "quadtari/quadtari.hpp"
|
||||
@@ -0,0 +1,38 @@
|
||||
Driving::Driving(Node::Port parent) {
|
||||
node = parent->append<Node::Peripheral>("Driving");
|
||||
|
||||
wheel = node->append<Node::Input::Axis>("Wheel");
|
||||
fire = node->append<Node::Input::Button>("Fire");
|
||||
}
|
||||
|
||||
auto Driving::poll() -> void {
|
||||
platform->input(wheel);
|
||||
|
||||
auto value = wheel->value();
|
||||
s32 nextDirection = value < -16384 ? -1 : value > 16384 ? 1 : 0;
|
||||
if(nextDirection && nextDirection != direction) {
|
||||
auto phase = position.bit(8, 9);
|
||||
if(nextDirection < 0) position = (phase + 4) * 256 - 1;
|
||||
if(nextDirection > 0) position = (phase + 1) * 256;
|
||||
} else if(nextDirection) {
|
||||
auto distance = value / 512 + (value > 0);
|
||||
position = (position + distance + 1024) & 1023;
|
||||
}
|
||||
direction = nextDirection;
|
||||
}
|
||||
|
||||
auto Driving::read() -> n8 {
|
||||
platform->input(fire);
|
||||
|
||||
static constexpr u8 gray[4] = {0x03, 0x01, 0x00, 0x02};
|
||||
|
||||
n8 data = 0xff;
|
||||
data.bit(0, 1) = gray[position.bit(8, 9)];
|
||||
data.bit(4) = !fire->value();
|
||||
return data;
|
||||
}
|
||||
|
||||
auto Driving::serialize(serializer& s) -> void {
|
||||
s(position);
|
||||
s(direction);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
struct Driving : Controller {
|
||||
Node::Input::Axis wheel;
|
||||
Node::Input::Button fire;
|
||||
|
||||
Driving(Node::Port);
|
||||
|
||||
auto poll() -> void override;
|
||||
auto read() -> n8 override;
|
||||
auto serialize(serializer&) -> void override;
|
||||
|
||||
private:
|
||||
n10 position = 0;
|
||||
s8 direction = 0;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
Gamepad::Gamepad(Node::Port parent) {
|
||||
node = parent->append<Node::Peripheral>("Gamepad");
|
||||
Gamepad::Gamepad(Node::Port parent, string name) {
|
||||
node = parent->append<Node::Peripheral>(name);
|
||||
|
||||
up = node->append<Node::Input::Button>("Up");
|
||||
down = node->append<Node::Input::Button>("Down");
|
||||
@@ -15,23 +15,11 @@ auto Gamepad::read() -> n8 {
|
||||
platform->input(right);
|
||||
platform->input(fire);
|
||||
|
||||
if(!(up->value() & down->value())) {
|
||||
yHold = 0, upLatch = up->value(), downLatch = down->value();
|
||||
} else if(!yHold) {
|
||||
yHold = 1, swap(upLatch, downLatch);
|
||||
}
|
||||
|
||||
if(!(left->value() & right->value())) {
|
||||
xHold = 0, leftLatch = left->value(), rightLatch = right->value();
|
||||
} else if(!xHold) {
|
||||
xHold = 1, swap(leftLatch, rightLatch);
|
||||
}
|
||||
|
||||
n8 data = 0xff;
|
||||
data.bit(0) = !upLatch;
|
||||
data.bit(1) = !downLatch;
|
||||
data.bit(2) = !leftLatch;
|
||||
data.bit(3) = !rightLatch;
|
||||
data.bit(0) = !up->value();
|
||||
data.bit(1) = !down->value();
|
||||
data.bit(2) = !left->value();
|
||||
data.bit(3) = !right->value();
|
||||
data.bit(4) = !fire->value();
|
||||
return data;
|
||||
}
|
||||
@@ -5,15 +5,7 @@ struct Gamepad : Controller {
|
||||
Node::Input::Button right;
|
||||
Node::Input::Button fire;
|
||||
|
||||
Gamepad(Node::Port);
|
||||
Gamepad(Node::Port, string name = "Gamepad");
|
||||
|
||||
auto read() -> n8 override;
|
||||
|
||||
private:
|
||||
b1 yHold;
|
||||
b1 upLatch;
|
||||
b1 downLatch;
|
||||
b1 xHold;
|
||||
b1 leftLatch;
|
||||
b1 rightLatch;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
Joy2BPlus::Joy2BPlus(Node::Port parent) : Gamepad(parent, "Joy 2B+") {
|
||||
buttonC = node->append<Node::Input::Button>("C");
|
||||
button3 = node->append<Node::Input::Button>("Button 3");
|
||||
}
|
||||
|
||||
auto Joy2BPlus::readAnalog(n1 index) -> AnalogConnection {
|
||||
auto& button = index ? buttonC : button3;
|
||||
platform->input(button);
|
||||
return button->value() ? AnalogConnection::ground(330) : AnalogConnection::vcc();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
struct Joy2BPlus : Gamepad {
|
||||
Node::Input::Button buttonC;
|
||||
Node::Input::Button button3;
|
||||
|
||||
Joy2BPlus(Node::Port);
|
||||
|
||||
auto readAnalog(n1 index) -> AnalogConnection override;
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
Keyboard::Keyboard(Node::Port parent) {
|
||||
node = parent->append<Node::Peripheral>("Keyboard");
|
||||
|
||||
static constexpr const char* names[12] = {
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#"
|
||||
};
|
||||
for(u32 index : range(12)) key[index] = node->append<Node::Input::Button>(names[index]);
|
||||
}
|
||||
|
||||
auto Keyboard::read() -> n8 {
|
||||
n8 data = 0xff;
|
||||
data.bit(4) = readColumn(2) != ColumnState::Ground;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto Keyboard::write(n8 data) -> void {
|
||||
rows = data.bit(0, 3);
|
||||
}
|
||||
|
||||
auto Keyboard::readAnalog(n1 index) -> AnalogConnection {
|
||||
switch(readColumn(index)) {
|
||||
case ColumnState::Pullup: return AnalogConnection::vcc(4700);
|
||||
case ColumnState::Ground: return AnalogConnection::ground();
|
||||
case ColumnState::Vcc: return AnalogConnection::vcc();
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
|
||||
auto Keyboard::readColumn(u32 column) -> ColumnState {
|
||||
bool vcc = false;
|
||||
for(u32 row : range(4)) {
|
||||
auto& button = key[row * 3 + column];
|
||||
platform->input(button);
|
||||
if(!button->value()) continue;
|
||||
if(!rows.bit(row)) return ColumnState::Ground;
|
||||
vcc = true;
|
||||
}
|
||||
return vcc ? ColumnState::Vcc : ColumnState::Pullup;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
struct Keyboard : Controller {
|
||||
Node::Input::Button key[12];
|
||||
|
||||
Keyboard(Node::Port);
|
||||
|
||||
auto read() -> n8 override;
|
||||
auto write(n8 data) -> void override;
|
||||
auto readAnalog(n1 index) -> AnalogConnection override;
|
||||
|
||||
private:
|
||||
enum class ColumnState : u32 { Pullup, Ground, Vcc };
|
||||
|
||||
auto readColumn(u32 column) -> ColumnState;
|
||||
|
||||
n4 rows = 0x0f;
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
auto KidVidAudio::load(VFS::Pak pak) -> void {
|
||||
static constexpr u64 MaximumFileSize = 64_MiB;
|
||||
static constexpr std::array<const char*, (u32)File::Count> Names = {
|
||||
"KVS3.WAV", "KVS1.WAV", "KVS2.WAV",
|
||||
"KVB3.WAV", "KVB1.WAV", "KVB2.WAV",
|
||||
"KVSHARED.WAV",
|
||||
};
|
||||
|
||||
reset();
|
||||
sources = {};
|
||||
mediaIdentity = 14695981039346656037ull;
|
||||
for(u32 index : range((u32)File::Count)) {
|
||||
auto fp = pak ? pak->read(Names[index]) : VFS::File{};
|
||||
if(!fp) continue;
|
||||
if(fp->size() > MaximumFileSize) continue;
|
||||
auto& item = sources[index];
|
||||
item.bytes.resize(fp->size());
|
||||
if(!item.bytes.empty()) memory::copy(item.bytes.data(), fp->data(), item.bytes.size());
|
||||
for(auto byte : item.bytes) {
|
||||
mediaIdentity ^= byte;
|
||||
mediaIdentity *= 1099511628211ull;
|
||||
}
|
||||
mediaIdentity ^= index;
|
||||
mediaIdentity *= 1099511628211ull;
|
||||
item.valid = parse(item);
|
||||
}
|
||||
}
|
||||
|
||||
auto KidVidAudio::reset() -> void {
|
||||
activeFile = File::Shared;
|
||||
cursor = 0;
|
||||
end = 0;
|
||||
active = false;
|
||||
}
|
||||
|
||||
auto KidVidAudio::available(File file) const -> bool {
|
||||
return file < File::Count && source(file).valid;
|
||||
}
|
||||
|
||||
auto KidVidAudio::play(File file, u32 begin, u32 end) -> bool {
|
||||
stop();
|
||||
if(!available(file)) return false;
|
||||
auto& item = source(file);
|
||||
if(begin < item.dataBegin || end < begin || end > item.dataEnd) return false;
|
||||
activeFile = file;
|
||||
cursor = begin;
|
||||
this->end = end;
|
||||
active = cursor < end;
|
||||
return active;
|
||||
}
|
||||
|
||||
auto KidVidAudio::stop() -> void {
|
||||
active = false;
|
||||
cursor = end;
|
||||
}
|
||||
|
||||
auto KidVidAudio::remaining() const -> u32 {
|
||||
if(!active || cursor >= end) return 0;
|
||||
return end - cursor;
|
||||
}
|
||||
|
||||
auto KidVidAudio::clock() -> f64 {
|
||||
if(!active) return 0.0;
|
||||
auto& item = source(activeFile);
|
||||
if(cursor >= end || cursor >= item.bytes.size()) {
|
||||
stop();
|
||||
return 0.0;
|
||||
}
|
||||
auto sample = item.bytes[cursor++];
|
||||
if(cursor >= end) active = false;
|
||||
return ((s32)sample - 128) / 128.0;
|
||||
}
|
||||
|
||||
auto KidVidAudio::serialize(serializer& s) -> bool {
|
||||
auto identity = mediaIdentity;
|
||||
u32 file = (u32)activeFile;
|
||||
s(identity);
|
||||
s(file);
|
||||
s(cursor);
|
||||
s(end);
|
||||
s(active);
|
||||
|
||||
if(s.reading()) {
|
||||
if(identity != mediaIdentity || file >= (u32)File::Count) {
|
||||
reset();
|
||||
return false;
|
||||
}
|
||||
activeFile = (File)file;
|
||||
if(!active) {
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
auto& item = source(activeFile);
|
||||
if(!item.valid || cursor < item.dataBegin || end < cursor || end > item.dataEnd) {
|
||||
reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto KidVidAudio::parse(Source& source) -> bool {
|
||||
auto& bytes = source.bytes;
|
||||
if(bytes.size() < 12) return false;
|
||||
auto tag = [&](u32 offset, const char* text) {
|
||||
return offset <= bytes.size() - 4 && bytes[offset + 0] == text[0] && bytes[offset + 1] == text[1]
|
||||
&& bytes[offset + 2] == text[2] && bytes[offset + 3] == text[3];
|
||||
};
|
||||
auto read16 = [&](u32 offset) -> u16 {
|
||||
return (u16)bytes[offset] | (u16)bytes[offset + 1] << 8;
|
||||
};
|
||||
auto read32 = [&](u32 offset) -> u32 {
|
||||
return (u32)bytes[offset] | (u32)bytes[offset + 1] << 8
|
||||
| (u32)bytes[offset + 2] << 16 | (u32)bytes[offset + 3] << 24;
|
||||
};
|
||||
|
||||
if(!tag(0, "RIFF") || !tag(8, "WAVE")) return false;
|
||||
auto declaredEnd = (u64)read32(4) + 8;
|
||||
if(declaredEnd > bytes.size() || declaredEnd < 12) return false;
|
||||
|
||||
bool formatValid = false;
|
||||
bool dataFound = false;
|
||||
for(u64 offset = 12; offset + 8 <= declaredEnd;) {
|
||||
auto chunkSize = read32((u32)offset + 4);
|
||||
auto dataBegin = offset + 8;
|
||||
auto dataEnd = dataBegin + chunkSize;
|
||||
if(dataEnd > declaredEnd || dataEnd < dataBegin) return false;
|
||||
|
||||
if(tag((u32)offset, "fmt ")) {
|
||||
if(chunkSize < 16) return false;
|
||||
formatValid = read16((u32)dataBegin + 0) == 1
|
||||
&& read16((u32)dataBegin + 2) == 1
|
||||
&& read32((u32)dataBegin + 4) == 16000
|
||||
&& read16((u32)dataBegin + 12) == 1
|
||||
&& read16((u32)dataBegin + 14) == 8;
|
||||
}
|
||||
if(tag((u32)offset, "data") && !dataFound) {
|
||||
source.dataBegin = dataBegin;
|
||||
source.dataEnd = dataEnd;
|
||||
dataFound = true;
|
||||
}
|
||||
|
||||
offset = dataEnd;
|
||||
//Several canonical KidVid WAVs end on an odd-sized data chunk without a
|
||||
//physical pad byte. A pad is required before another chunk, not after EOF.
|
||||
if((chunkSize & 1) && offset < declaredEnd) offset++;
|
||||
if(offset > declaredEnd) return false;
|
||||
}
|
||||
return formatValid && dataFound && source.dataBegin < source.dataEnd;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
struct KidVidAudio {
|
||||
enum class File : u32 {
|
||||
Smurfs3,
|
||||
Smurfs1,
|
||||
Smurfs2,
|
||||
Bears3,
|
||||
Bears1,
|
||||
Bears2,
|
||||
Shared,
|
||||
Count,
|
||||
};
|
||||
|
||||
auto load(VFS::Pak) -> void;
|
||||
auto reset() -> void;
|
||||
auto available(File) const -> bool;
|
||||
auto play(File, u32 begin, u32 end) -> bool;
|
||||
auto stop() -> void;
|
||||
auto playing() const -> bool { return active; }
|
||||
auto remaining() const -> u32;
|
||||
auto clock() -> f64;
|
||||
auto serialize(serializer&) -> bool;
|
||||
|
||||
private:
|
||||
struct Source {
|
||||
std::vector<u8> bytes;
|
||||
u32 dataBegin = 0;
|
||||
u32 dataEnd = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
auto parse(Source&) -> bool;
|
||||
auto source(File file) -> Source& { return sources[(u32)file]; }
|
||||
auto source(File file) const -> const Source& { return sources[(u32)file]; }
|
||||
|
||||
std::array<Source, (u32)File::Count> sources;
|
||||
u64 mediaIdentity = 0;
|
||||
File activeFile = File::Shared;
|
||||
u32 cursor = 0;
|
||||
u32 end = 0;
|
||||
bool active = false;
|
||||
};
|
||||
@@ -0,0 +1,216 @@
|
||||
#include "tables.hpp"
|
||||
|
||||
static constexpr const char* KidVidSmurfsSha256 =
|
||||
"bbe0f1d952aab334de82db18cd84feb49a7fd9beb60a39c52658483564af78be";
|
||||
static constexpr const char* KidVidBearsSha256 =
|
||||
"6161ac39cddfe7773c553f1b7a5925b703025ebe6ac18e934b8eb94465ed4548";
|
||||
|
||||
KidVid::KidVid(Node::Port parent) {
|
||||
node = parent->append<Node::Peripheral>("KidVid Voice Module");
|
||||
game1 = node->append<Node::Input::Button>("Game 1");
|
||||
game2 = node->append<Node::Input::Button>("Game 2");
|
||||
game3 = node->append<Node::Input::Button>("Game 3");
|
||||
skip = node->append<Node::Input::Button>("Skip");
|
||||
|
||||
stream = node->append<Node::Audio::Stream>("KidVid");
|
||||
stream->setChannels(1);
|
||||
stream->setFrequency(16000);
|
||||
|
||||
node->setPak(pak = platform->pak(node));
|
||||
audio.load(pak);
|
||||
if(cartridge.sha256() == KidVidSmurfsSha256) profile = Profile::Smurfs;
|
||||
if(cartridge.sha256() == KidVidBearsSha256) profile = Profile::Bears;
|
||||
rewind();
|
||||
Thread::create(16000, std::bind_front(&KidVid::main, this));
|
||||
}
|
||||
|
||||
KidVid::~KidVid() {
|
||||
Thread::destroy();
|
||||
stream.reset();
|
||||
pak.reset();
|
||||
}
|
||||
|
||||
auto KidVid::main() -> void {
|
||||
stream->frame(audio.clock());
|
||||
Thread::step(1);
|
||||
Thread::synchronize(cpu);
|
||||
}
|
||||
|
||||
auto KidVid::power(bool reset) -> void {
|
||||
if(!reset || profile == Profile::Smurfs) rewind();
|
||||
Thread::create(16000, std::bind_front(&KidVid::main, this));
|
||||
}
|
||||
|
||||
auto KidVid::poll() -> void {
|
||||
platform->input(game1);
|
||||
platform->input(game2);
|
||||
platform->input(game3);
|
||||
platform->input(skip);
|
||||
}
|
||||
|
||||
auto KidVid::frame() -> void {
|
||||
if(profile == Profile::Unknown) return;
|
||||
if(profile == Profile::Smurfs && system.controls.reset->value()) rewind();
|
||||
|
||||
if(skip->value() && song && KidVidData::ClipOrder[song - 1] != 0
|
||||
&& KidVidData::ClipOrder[song - 1] != 11) {
|
||||
audio.stop();
|
||||
}
|
||||
|
||||
if(!tape) {
|
||||
if(game1->value()) select(1);
|
||||
else if(game2->value()) select(2);
|
||||
else if(game3->value()) select(3);
|
||||
}
|
||||
|
||||
if(tape && run && !tapeBusy) advanceCassette();
|
||||
updatePlayback();
|
||||
}
|
||||
|
||||
auto KidVid::read() -> n8 {
|
||||
n8 lines = 0xff;
|
||||
lines.bit(3) = data;
|
||||
return lines;
|
||||
}
|
||||
|
||||
auto KidVid::write(n8 lines) -> void {
|
||||
run = lines.bit(0);
|
||||
}
|
||||
|
||||
auto KidVid::serialize(serializer& s) -> void {
|
||||
Thread::serialize(s);
|
||||
auto audioStateCompatible = audio.serialize(s);
|
||||
s(tape);
|
||||
s(bit);
|
||||
s(bitsRemaining);
|
||||
s(block);
|
||||
s(song);
|
||||
s(fallbackFrames);
|
||||
s(run);
|
||||
s(data);
|
||||
s(tapeBusy);
|
||||
s(songPlaying);
|
||||
s(beep);
|
||||
|
||||
if(s.reading()) {
|
||||
auto cassetteBits = KidVidData::CassetteData.size() * 8;
|
||||
if(tape > 4 || bit >= cassetteBits || !bitsRemaining || bitsRemaining > 48
|
||||
|| bitsRemaining > cassetteBits - bit || song > KidVidData::ClipOrder.size() || fallbackFrames > 92) {
|
||||
rewind();
|
||||
}
|
||||
mediaAvailable = tape && audio.available(KidVidAudio::File::Shared) && audio.available(tapeFile());
|
||||
if(!audioStateCompatible) {
|
||||
mediaAvailable = false;
|
||||
songPlaying = false;
|
||||
beep = true;
|
||||
tapeBusy = tape != 0;
|
||||
fallbackFrames = tape ? 92 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto KidVid::rewind() -> void {
|
||||
audio.reset();
|
||||
tape = 0;
|
||||
bit = profile == Profile::Bears ? 48 : 0;
|
||||
bitsRemaining = 48;
|
||||
block = 0;
|
||||
song = 0;
|
||||
fallbackFrames = 0;
|
||||
data = true;
|
||||
tapeBusy = false;
|
||||
songPlaying = false;
|
||||
beep = false;
|
||||
mediaAvailable = false;
|
||||
}
|
||||
|
||||
auto KidVid::select(u32 game) -> void {
|
||||
if(game < 1 || game > 3) return;
|
||||
tape = game == 1 ? 2 : game == 2 ? 3 : profile == Profile::Bears ? 4 : 1;
|
||||
bit = profile == Profile::Bears ? 48 : 0;
|
||||
bitsRemaining = 48;
|
||||
block = 0;
|
||||
song = KidVidData::FirstClip[tapeIndex()];
|
||||
fallbackFrames = 0;
|
||||
tapeBusy = false;
|
||||
songPlaying = false;
|
||||
beep = false;
|
||||
mediaAvailable = audio.available(KidVidAudio::File::Shared) && audio.available(tapeFile());
|
||||
}
|
||||
|
||||
auto KidVid::tapeIndex() const -> u32 {
|
||||
if(tape == 4) return 3;
|
||||
return profile == Profile::Smurfs ? tape - 1 : tape + 2;
|
||||
}
|
||||
|
||||
auto KidVid::tapeFile() const -> KidVidAudio::File {
|
||||
return (KidVidAudio::File)tapeIndex();
|
||||
}
|
||||
|
||||
auto KidVid::advanceCassette() -> void {
|
||||
auto byte = KidVidData::CassetteData[bit >> 3];
|
||||
data = byte >> (7 - (bit & 7)) & 1;
|
||||
bit++;
|
||||
if(--bitsRemaining) return;
|
||||
|
||||
if(block == 0) {
|
||||
bit = 96 + (tape - 1) * 48;
|
||||
} else if(block >= KidVidData::BlockCounts[tapeIndex()]) {
|
||||
bit = 336;
|
||||
} else {
|
||||
bit = 288;
|
||||
startNextClip();
|
||||
}
|
||||
block++;
|
||||
bitsRemaining = 48;
|
||||
}
|
||||
|
||||
auto KidVid::startNextClip() -> void {
|
||||
if(!mediaAvailable || song >= KidVidData::ClipOrder.size()) {
|
||||
beep = true;
|
||||
tapeBusy = true;
|
||||
songPlaying = false;
|
||||
fallbackFrames = 92;
|
||||
return;
|
||||
}
|
||||
|
||||
auto entry = KidVidData::ClipOrder[song++];
|
||||
beep = !(entry & 0x80);
|
||||
auto clip = entry & 0x7f;
|
||||
if(clip + 1 >= KidVidData::ClipBoundaries.size()) {
|
||||
mediaAvailable = false;
|
||||
return startNextClip();
|
||||
}
|
||||
|
||||
auto begin = KidVidData::ClipBoundaries[clip];
|
||||
auto end = KidVidData::ClipBoundaries[clip + 1];
|
||||
if(end < begin + 786) {
|
||||
mediaAvailable = false;
|
||||
return startNextClip();
|
||||
}
|
||||
auto file = clip < 10 ? KidVidAudio::File::Shared : tapeFile();
|
||||
if(!audio.play(file, begin, end - 786)) {
|
||||
mediaAvailable = false;
|
||||
return startNextClip();
|
||||
}
|
||||
songPlaying = true;
|
||||
tapeBusy = true;
|
||||
}
|
||||
|
||||
auto KidVid::updatePlayback() -> void {
|
||||
if(mediaAvailable) {
|
||||
if(!songPlaying) return;
|
||||
auto remaining = audio.remaining();
|
||||
tapeBusy = remaining > 262 * 60 || !beep;
|
||||
if(remaining) return;
|
||||
songPlaying = false;
|
||||
tapeBusy = !beep;
|
||||
if(!beep) startNextClip();
|
||||
return;
|
||||
}
|
||||
|
||||
if(fallbackFrames) {
|
||||
fallbackFrames--;
|
||||
tapeBusy = fallbackFrames > 60;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "audio.hpp"
|
||||
|
||||
struct KidVid : Controller, Thread {
|
||||
KidVid(Node::Port);
|
||||
~KidVid();
|
||||
|
||||
auto main() -> void;
|
||||
auto power(bool reset) -> void override;
|
||||
auto poll() -> void override;
|
||||
auto frame() -> void override;
|
||||
auto read() -> n8 override;
|
||||
auto write(n8 data) -> void override;
|
||||
auto serialize(serializer&) -> void override;
|
||||
|
||||
private:
|
||||
enum class Profile : u32 { Unknown, Smurfs, Bears };
|
||||
|
||||
auto rewind() -> void;
|
||||
auto select(u32 game) -> void;
|
||||
auto tapeIndex() const -> u32;
|
||||
auto tapeFile() const -> KidVidAudio::File;
|
||||
auto advanceCassette() -> void;
|
||||
auto startNextClip() -> void;
|
||||
auto updatePlayback() -> void;
|
||||
|
||||
Node::Input::Button game1;
|
||||
Node::Input::Button game2;
|
||||
Node::Input::Button game3;
|
||||
Node::Input::Button skip;
|
||||
Node::Audio::Stream stream;
|
||||
VFS::Pak pak;
|
||||
KidVidAudio audio;
|
||||
|
||||
Profile profile = Profile::Unknown;
|
||||
u32 tape = 0;
|
||||
u32 bit = 0;
|
||||
u32 bitsRemaining = 48;
|
||||
u32 block = 0;
|
||||
u32 song = 0;
|
||||
u32 fallbackFrames = 0;
|
||||
bool run = true;
|
||||
bool data = true;
|
||||
bool tapeBusy = false;
|
||||
bool songPlaying = false;
|
||||
bool beep = false;
|
||||
bool mediaAvailable = false;
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
namespace KidVidData {
|
||||
|
||||
//Connector data recorded by the two supported programs. Each cassette block
|
||||
//is six bytes and is shifted most-significant bit first.
|
||||
static constexpr std::array<u8, 48> CassetteData = {
|
||||
0x7b, 0x1e, 0xc6, 0x31, 0xec, 0x60,
|
||||
0x7b, 0x1e, 0xc6, 0x3d, 0x8c, 0x60,
|
||||
0xf6, 0x31, 0x8c, 0x63, 0x18, 0xc0,
|
||||
0xf6, 0x31, 0x8c, 0x63, 0x18, 0xf0,
|
||||
0xf6, 0x31, 0x8c, 0x63, 0x1e, 0xc0,
|
||||
0xf6, 0x31, 0x8c, 0x63, 0x1e, 0xf0,
|
||||
0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00,
|
||||
0xf7, 0xb1, 0x8c, 0x63, 0x18, 0xc0,
|
||||
};
|
||||
|
||||
//Order: Smurfs 3/1/2, then Bears 3/1/2.
|
||||
static constexpr std::array<u8, 6> BlockCounts = {42, 23, 37, 102, 120, 102};
|
||||
static constexpr std::array<u16, 6> FirstClip = {82, 0, 44, 266, 124, 186};
|
||||
|
||||
//The high bit marks a segment that ends without the cassette acknowledgement
|
||||
//beep. The low seven bits select adjacent boundaries from ClipBoundaries.
|
||||
static constexpr std::array<u8, 328> ClipOrder = {
|
||||
11, 12+0x80, 13+0x80, 14, 15+0x80, 16, 8+0x80, 17, 18+0x80, 19, 20+0x80,
|
||||
21, 8+0x80, 22, 15+0x80, 23, 18+0x80, 14, 20+0x80, 16, 18+0x80,
|
||||
17, 15+0x80, 19, 8+0x80, 21, 20+0x80, 22, 18+0x80, 23, 15+0x80,
|
||||
14, 20+0x80, 16, 8+0x80, 22, 15+0x80, 23, 18+0x80, 14, 20+0x80,
|
||||
16, 8+0x80, 9,
|
||||
|
||||
25+0x80, 26, 27, 28, 8, 29, 30, 26, 27, 28, 8, 29, 30, 26, 27, 28, 8, 29,
|
||||
30, 26, 27, 28, 8, 29, 30, 26, 27, 28, 8, 29, 30, 26, 27, 28, 8, 29,
|
||||
30+0x80, 9,
|
||||
|
||||
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 34, 42, 36, 43, 40, 39, 38, 37,
|
||||
34, 43, 36, 39, 40, 37, 38, 43, 34, 37, 36, 43, 40, 39, 38, 37, 34, 43,
|
||||
36, 39, 40, 37, 38+0x80, 9,
|
||||
|
||||
0, 1, 45, 2, 3, 46, 4, 5, 47, 6, 7, 48, 4, 3, 49, 2, 1, 50, 6, 7, 51,
|
||||
4, 5, 52, 6, 1, 53, 2, 7, 54, 6, 5, 45, 2, 1, 46, 4, 3, 47, 2, 5, 48,
|
||||
4, 7, 49, 6, 1, 50, 2, 5, 51, 6, 3, 52, 4, 7, 53, 2, 1, 54, 6+0x80, 9,
|
||||
|
||||
0, 1, 56, 4, 3, 57, 2, 5, 58, 6, 7, 59, 2, 3, 60, 4, 1, 61, 6, 7, 62,
|
||||
2, 5, 63, 6, 1, 64, 4, 7, 65, 6, 5, 66, 4, 1, 67, 2, 3, 68, 6, 5, 69,
|
||||
2, 7, 70, 4, 1, 71, 2, 5, 72, 4, 3, 73, 6, 7, 74, 2, 1, 75, 6, 3, 76,
|
||||
4, 5, 77, 6, 7, 78, 2, 3, 79, 4, 1, 80, 2, 7, 81, 4+0x80, 9,
|
||||
|
||||
0, 1, 83, 2, 3, 84, 4, 5, 85, 6, 7, 86, 4, 3, 87, 2, 1, 88, 6, 7, 89,
|
||||
2, 5, 90, 6, 1, 91, 4, 7, 92, 6, 5, 93, 4, 1, 94, 2, 3, 95, 6, 5, 96,
|
||||
2, 7, 97, 4, 1, 98, 6, 5, 99, 4, 3, 100, 2, 7, 101, 4, 1, 102, 2+0x80, 9,
|
||||
};
|
||||
|
||||
//Absolute byte offsets in the canonical RIFF files. Indexes 0..10 belong to
|
||||
//KVSHARED; every later index belongs to the selected tape file.
|
||||
static constexpr std::array<u32, 104> ClipBoundaries = {
|
||||
44, 980829, 1178398, 1430063, 1691136, 1841665, 2100386, 2283843, 2629588, 2824805, 3059116,
|
||||
|
||||
44, 164685, 395182, 750335, 962016, 1204273, 1538258, 1801683, 2086276, 2399093,
|
||||
2589606, 2801287, 3111752, 3436329,
|
||||
|
||||
44, 778557, 1100782, 1293648, 1493569, 1801682, 2086275,
|
||||
|
||||
44, 686829, 893806, 1143119, 1385376, 1578241, 1942802, 2168595, 2493172,
|
||||
2662517, 3022374, 3229351, 3720920,
|
||||
|
||||
44, 592749, 936142, 1465343, 1787568, 2145073, 2568434, 2822451, 3045892, 3709157, 4219542,
|
||||
|
||||
44, 303453, 703294, 1150175, 1514736, 2208577, 2511986, 2864787, 3306964, 3864389,
|
||||
4148982, 4499431, 4824008, 5162697, 5581354, 5844779, 6162300, 6590365, 6839678,
|
||||
7225407, 7552336, 7867505, 8316738, 8608387, 8940020, 9274005, 9593878,
|
||||
|
||||
44, 341085, 653902, 1018463, 1265424, 1669969, 1919282, 2227395, 2535508, 3057653,
|
||||
3375174, 3704455, 4092536, 4487673, 5026282, 5416715, 5670732, 6225805, 6736190,
|
||||
7110159, 7676992,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
MindLink::MindLink(Node::Port parent) {
|
||||
node = parent->append<Node::Peripheral>("MindLink");
|
||||
|
||||
x = node->append<Node::Input::Axis>("X");
|
||||
trigger = node->append<Node::Input::Button>("Trigger");
|
||||
}
|
||||
|
||||
auto MindLink::poll() -> void {
|
||||
platform->input(x);
|
||||
platform->input(trigger);
|
||||
|
||||
auto next = (s64)(position & ~TriggerValue) + x->value() * PositionScale;
|
||||
position = std::clamp<s64>(next, MinimumPosition, MaximumPosition);
|
||||
|
||||
//Stella exposes this compatibility trigger, but the physical MindLink had no button.
|
||||
if(trigger->value()) position |= TriggerValue;
|
||||
|
||||
lines = 0x1f;
|
||||
shift = 1;
|
||||
nextBit();
|
||||
}
|
||||
|
||||
auto MindLink::read() -> n8 {
|
||||
n8 data = 0xff;
|
||||
data.bit(0, 4) = lines;
|
||||
return data;
|
||||
}
|
||||
|
||||
auto MindLink::write(n8 data) -> void {
|
||||
lines.bit(0, 3) = data.bit(0, 3);
|
||||
}
|
||||
|
||||
auto MindLink::controlWrite(n8 data) -> void {
|
||||
if(data.bit(0)) nextBit();
|
||||
}
|
||||
|
||||
auto MindLink::serialize(serializer& s) -> void {
|
||||
s(position);
|
||||
s(shift);
|
||||
s(lines);
|
||||
}
|
||||
|
||||
auto MindLink::nextBit() -> void {
|
||||
lines.bit(2) = 0;
|
||||
lines.bit(3) = (position & shift) != 0;
|
||||
shift <<= 1;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
struct MindLink : Controller {
|
||||
Node::Input::Axis x;
|
||||
Node::Input::Button trigger;
|
||||
|
||||
MindLink(Node::Port);
|
||||
|
||||
auto poll() -> void override;
|
||||
auto read() -> n8 override;
|
||||
auto write(n8 data) -> void override;
|
||||
auto controlWrite(n8 data) -> void override;
|
||||
auto serialize(serializer&) -> void override;
|
||||
|
||||
private:
|
||||
auto nextBit() -> void;
|
||||
|
||||
u32 position = 0x2a00;
|
||||
u32 shift = 1;
|
||||
n5 lines = 0x1f;
|
||||
|
||||
static constexpr s32 PositionScale = 10;
|
||||
static constexpr s32 MinimumPosition = 0x0b00;
|
||||
static constexpr s32 MaximumPosition = 0x6500;
|
||||
static constexpr u32 TriggerValue = 0x8000;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
Paddles::Paddles(Node::Port parent, bool axes) {
|
||||
node = parent->append<Node::Peripheral>("Paddles");
|
||||
|
||||
if(axes) {
|
||||
axis[0] = node->append<Node::Input::Axis>("Paddle 1");
|
||||
axis[1] = node->append<Node::Input::Axis>("Paddle 2");
|
||||
}
|
||||
fire[0] = node->append<Node::Input::Button>("Paddle 1 Fire");
|
||||
fire[1] = node->append<Node::Input::Button>("Paddle 2 Fire");
|
||||
}
|
||||
|
||||
auto Paddles::read() -> n8 {
|
||||
platform->input(fire[0]);
|
||||
platform->input(fire[1]);
|
||||
|
||||
n8 data = 0xff;
|
||||
data.bit(3) = !fire[0]->value();
|
||||
data.bit(2) = !fire[1]->value();
|
||||
return data;
|
||||
}
|
||||
|
||||
auto Paddles::readAnalog(n1 index) -> AnalogConnection {
|
||||
if(!axis[index]) return AnalogConnection::disconnected();
|
||||
platform->input(axis[index]);
|
||||
auto value = std::clamp<s64>(axis[index]->value(), -32768, 32767);
|
||||
auto resistance = (u64)(32767 - value) * MaximumResistance / 65535;
|
||||
return AnalogConnection::vcc(resistance);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
struct Paddles : Controller {
|
||||
Node::Input::Axis axis[2];
|
||||
Node::Input::Button fire[2];
|
||||
|
||||
Paddles(Node::Port, bool axes = true);
|
||||
|
||||
auto read() -> n8 override;
|
||||
auto readAnalog(n1 index) -> AnalogConnection override;
|
||||
|
||||
static constexpr u32 MaximumResistance = 1'000'000;
|
||||
};
|
||||
@@ -10,20 +10,61 @@ auto ControllerPort::load(Node::Object parent) -> void {
|
||||
port->setType("Controller");
|
||||
port->setHotSwappable(true);
|
||||
port->setAllocate([&](auto name) { return allocate(name); });
|
||||
port->setDisconnect([&] { device.reset(); });
|
||||
port->setSupported({"Gamepad"});
|
||||
port->setDisconnect([&] {
|
||||
save();
|
||||
device.reset();
|
||||
});
|
||||
std::vector<string> supported = {
|
||||
"Gamepad", "Paddles", "Driving", "Keyboard",
|
||||
"Booster Grip", "Sega Genesis", "Joy 2B+",
|
||||
"CX-22 Trak-Ball", "CX-80 Trak-Ball", "Atari Mouse", "Amiga Mouse",
|
||||
"XG-1 Light Gun", "MindLink", "SaveKey", "AtariVox", "QuadTari",
|
||||
};
|
||||
if(name == "Controller Port 2") supported.push_back("KidVid Voice Module");
|
||||
port->setSupported(supported);
|
||||
output = 0x0f;
|
||||
}
|
||||
|
||||
auto ControllerPort::unload() -> void {
|
||||
device = {};
|
||||
port = {};
|
||||
output = 0x0f;
|
||||
}
|
||||
|
||||
auto ControllerPort::allocate(string name) -> Node::Peripheral {
|
||||
if(name == "Gamepad") device = new Gamepad(port);
|
||||
if(device) return device->node;
|
||||
auto role = this == &controllerPort2 ? Role::ConsoleRight : Role::ConsoleLeft;
|
||||
device = create(port, name, role);
|
||||
if(device) {
|
||||
device->write(output);
|
||||
return device->node;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto ControllerPort::create(Node::Port port, string name, Role role) -> std::unique_ptr<Controller> {
|
||||
auto quadTariChild = role == Role::QuadTariChild;
|
||||
if(name == "Gamepad") return std::make_unique<Gamepad> (port);
|
||||
if(name == "Paddles") return std::make_unique<Paddles> (port, !quadTariChild);
|
||||
if(name == "Driving") return std::make_unique<Driving> (port);
|
||||
if(name == "Keyboard") return std::make_unique<Keyboard> (port);
|
||||
if(name == "Booster Grip") return std::make_unique<BoosterGrip>(port);
|
||||
if(name == "Sega Genesis") return std::make_unique<SegaGenesis>(port);
|
||||
if(name == "Joy 2B+") return std::make_unique<Joy2BPlus> (port);
|
||||
if(name == "CX-22 Trak-Ball") return std::make_unique<TrakBall> (port, name);
|
||||
if(name == "CX-80 Trak-Ball") return std::make_unique<TrakBall> (port, name);
|
||||
if(name == "Atari Mouse") return std::make_unique<AtariMouse> (port);
|
||||
if(name == "Amiga Mouse") return std::make_unique<AmigaMouse> (port);
|
||||
if(name == "XG-1 Light Gun") return std::make_unique<XG1LightGun>(port);
|
||||
if(name == "MindLink") return std::make_unique<MindLink> (port);
|
||||
if(name == "SaveKey") return std::make_unique<SaveKey> (port);
|
||||
if(name == "AtariVox") return std::make_unique<AtariVox> (port);
|
||||
if(name == "KidVid Voice Module")
|
||||
if(role == Role::ConsoleRight) return std::make_unique<KidVid> (port);
|
||||
if(name == "QuadTari")
|
||||
if(!quadTariChild) return std::make_unique<QuadTari> (port);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto ControllerPort::serialize(serializer& s) -> void {
|
||||
if(device) device->serialize(s);
|
||||
}
|
||||
@@ -1,19 +1,44 @@
|
||||
struct ControllerPort {
|
||||
enum class Role : u32 { ConsoleLeft, ConsoleRight, QuadTariChild };
|
||||
|
||||
Node::Port port;
|
||||
unique_pointer<Controller> device;
|
||||
std::unique_ptr<Controller> device;
|
||||
|
||||
//port.cpp
|
||||
ControllerPort(string name);
|
||||
auto load(Node::Object) -> void;
|
||||
auto unload() -> void;
|
||||
auto allocate(string name) -> Node::Peripheral;
|
||||
static auto create(Node::Port, string name, Role) -> std::unique_ptr<Controller>;
|
||||
auto save() -> void { if(device) device->save(); }
|
||||
auto power(bool reset) -> void {
|
||||
if(device) {
|
||||
device->power(reset);
|
||||
device->write(output);
|
||||
}
|
||||
}
|
||||
|
||||
auto poll() -> void { if(device) device->poll(); }
|
||||
auto frame() -> void { if(device) device->frame(); }
|
||||
auto clock() -> void { if(device) device->clock(); }
|
||||
auto vblank(n1 dumped) -> void { if(device) device->vblank(dumped); }
|
||||
auto read() -> n8 { if(device) return device->read(); return 0xff; }
|
||||
auto write(n8 data) -> void { if(device) return device->write(data); }
|
||||
auto write(n8 data) -> void {
|
||||
output = data;
|
||||
if(device) device->write(data);
|
||||
}
|
||||
auto controlWrite(n4 data) -> void {
|
||||
if(device) device->controlWrite(data);
|
||||
}
|
||||
auto readAnalog(n1 index) -> Controller::AnalogConnection {
|
||||
if(device) return device->readAnalog(index);
|
||||
return Controller::AnalogConnection::disconnected();
|
||||
}
|
||||
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
const string name;
|
||||
n4 output = 0x0f;
|
||||
};
|
||||
|
||||
extern ControllerPort controllerPort1;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
QuadTari::QuadTari(Node::Port parent) : first(*this), second(*this) {
|
||||
node = parent->append<Node::Peripheral>("QuadTari");
|
||||
|
||||
auto left = parent->name() == "Controller Port 1";
|
||||
first.load(node, left ? "QuadTari Player 1" : "QuadTari Player 2");
|
||||
second.load(node, left ? "QuadTari Player 3" : "QuadTari Player 4");
|
||||
}
|
||||
|
||||
QuadTari::~QuadTari() {
|
||||
first.unload();
|
||||
second.unload();
|
||||
}
|
||||
|
||||
auto QuadTari::Slot::load(Node::Peripheral parent, string name) -> void {
|
||||
port = parent->append<Node::Port>(name);
|
||||
port->setFamily(system.name());
|
||||
port->setType("Controller");
|
||||
port->setHotSwappable(true);
|
||||
port->setAllocate([&](auto name) { return allocate(name); });
|
||||
port->setDisconnect([&] {
|
||||
save();
|
||||
device.reset();
|
||||
});
|
||||
port->setSupported({"Gamepad", "Paddles", "Driving", "SaveKey", "AtariVox"});
|
||||
port->allocate("Gamepad");
|
||||
port->connect();
|
||||
}
|
||||
|
||||
auto QuadTari::Slot::unload() -> void {
|
||||
save();
|
||||
device = {};
|
||||
port = {};
|
||||
}
|
||||
|
||||
auto QuadTari::Slot::allocate(string name) -> Node::Peripheral {
|
||||
save();
|
||||
device = {};
|
||||
if(name != "Gamepad" && name != "Paddles" && name != "Driving" && name != "SaveKey" && name != "AtariVox") {
|
||||
return {};
|
||||
}
|
||||
device = ControllerPort::create(port, name, ControllerPort::Role::QuadTariChild);
|
||||
if(device) {
|
||||
device->write(owner.output);
|
||||
device->controlWrite(owner.control);
|
||||
return device->node;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto QuadTari::Slot::save() -> void {
|
||||
if(device) device->save();
|
||||
}
|
||||
|
||||
auto QuadTari::Slot::power(bool reset) -> void {
|
||||
if(device) device->power(reset);
|
||||
}
|
||||
|
||||
auto QuadTari::Slot::serialize(serializer& s) -> void {
|
||||
if(device) device->serialize(s);
|
||||
}
|
||||
|
||||
auto QuadTari::save() -> void {
|
||||
first.save();
|
||||
second.save();
|
||||
}
|
||||
|
||||
auto QuadTari::power(bool reset) -> void {
|
||||
first.power(reset);
|
||||
second.power(reset);
|
||||
dumped = 0;
|
||||
selected = 0;
|
||||
pending = 0;
|
||||
settling = 0;
|
||||
output = 0x0f;
|
||||
control = 0x0f;
|
||||
}
|
||||
|
||||
auto QuadTari::poll() -> void {
|
||||
if(first.device) first.device->poll();
|
||||
if(second.device) second.device->poll();
|
||||
}
|
||||
|
||||
auto QuadTari::frame() -> void {
|
||||
if(first.device) first.device->frame();
|
||||
if(second.device) second.device->frame();
|
||||
}
|
||||
|
||||
auto QuadTari::clock() -> void {
|
||||
if(settling && !--settling) {
|
||||
selected = pending;
|
||||
synchronizeActive();
|
||||
}
|
||||
}
|
||||
|
||||
auto QuadTari::vblank(n1 dumped) -> void {
|
||||
if(this->dumped == dumped) return;
|
||||
this->dumped = dumped;
|
||||
pending = dumped;
|
||||
settling = SettlingClocks;
|
||||
}
|
||||
|
||||
auto QuadTari::read() -> n8 {
|
||||
if(auto device = active()) return device->read();
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
auto QuadTari::write(n8 data) -> void {
|
||||
output = data;
|
||||
if(auto device = active()) device->write(data);
|
||||
}
|
||||
|
||||
auto QuadTari::controlWrite(n8 data) -> void {
|
||||
control = data;
|
||||
if(auto device = active()) device->controlWrite(data);
|
||||
}
|
||||
|
||||
auto QuadTari::readAnalog(n1 index) -> AnalogConnection {
|
||||
return index ? AnalogConnection::vcc() : AnalogConnection::ground();
|
||||
}
|
||||
|
||||
auto QuadTari::serialize(serializer& s) -> void {
|
||||
s(dumped);
|
||||
s(selected);
|
||||
s(pending);
|
||||
s(settling);
|
||||
s(output);
|
||||
s(control);
|
||||
first.serialize(s);
|
||||
second.serialize(s);
|
||||
|
||||
if(s.reading() && settling > SettlingClocks) {
|
||||
dumped = 0;
|
||||
selected = 0;
|
||||
pending = 0;
|
||||
settling = 0;
|
||||
output = 0x0f;
|
||||
control = 0x0f;
|
||||
synchronizeActive();
|
||||
}
|
||||
}
|
||||
|
||||
auto QuadTari::active() -> Controller* {
|
||||
auto& slot = selected ? second : first;
|
||||
return slot.device.get();
|
||||
}
|
||||
|
||||
auto QuadTari::synchronizeActive() -> void {
|
||||
if(auto device = active()) {
|
||||
device->write(output);
|
||||
device->controlWrite(control);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
struct QuadTari : Controller {
|
||||
struct Slot {
|
||||
Slot(QuadTari& owner) : owner(owner) {}
|
||||
|
||||
auto load(Node::Peripheral, string name) -> void;
|
||||
auto unload() -> void;
|
||||
auto allocate(string name) -> Node::Peripheral;
|
||||
auto save() -> void;
|
||||
auto power(bool reset) -> void;
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
QuadTari& owner;
|
||||
Node::Port port;
|
||||
std::unique_ptr<Controller> device;
|
||||
};
|
||||
|
||||
QuadTari(Node::Port);
|
||||
~QuadTari() override;
|
||||
|
||||
auto save() -> void override;
|
||||
auto power(bool reset) -> void override;
|
||||
auto poll() -> void override;
|
||||
auto frame() -> void override;
|
||||
auto clock() -> void override;
|
||||
auto vblank(n1 dumped) -> void override;
|
||||
auto read() -> n8 override;
|
||||
auto write(n8 data) -> void override;
|
||||
auto controlWrite(n8 data) -> void override;
|
||||
auto readAnalog(n1 index) -> AnalogConnection override;
|
||||
auto serialize(serializer&) -> void override;
|
||||
|
||||
static constexpr u16 SettlingClocks = 228;
|
||||
|
||||
private:
|
||||
auto active() -> Controller*;
|
||||
auto synchronizeActive() -> void;
|
||||
|
||||
public:
|
||||
Slot first;
|
||||
Slot second;
|
||||
n1 dumped = 0;
|
||||
n1 selected = 0;
|
||||
n1 pending = 0;
|
||||
u16 settling = 0;
|
||||
n4 output = 0x0f;
|
||||
n4 control = 0x0f;
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
RelativePointingDevice::RelativePointingDevice(Node::Port parent, string name, u32 scaleNumerator)
|
||||
: scaleNumerator(scaleNumerator) {
|
||||
node = parent->append<Node::Peripheral>(name);
|
||||
|
||||
x = node->append<Node::Input::Axis>("X");
|
||||
y = node->append<Node::Input::Axis>("Y");
|
||||
fire = node->append<Node::Input::Button>("Fire");
|
||||
}
|
||||
|
||||
auto RelativePointingDevice::poll() -> void {
|
||||
finish(horizontal);
|
||||
finish(vertical);
|
||||
|
||||
platform->input(x);
|
||||
platform->input(y);
|
||||
queue(horizontal, -x->value());
|
||||
queue(vertical, -y->value());
|
||||
}
|
||||
|
||||
auto RelativePointingDevice::read() -> n8 {
|
||||
advance(horizontal, cpu.clock());
|
||||
advance(vertical, cpu.clock());
|
||||
platform->input(fire);
|
||||
|
||||
n8 data = 0xff;
|
||||
data.bit(0, 3) = encode(horizontal.phase, vertical.phase,
|
||||
horizontal.direction < 0, vertical.direction > 0);
|
||||
data.bit(4) = !fire->value();
|
||||
return data;
|
||||
}
|
||||
|
||||
auto RelativePointingDevice::finish(AxisState& axis) -> void {
|
||||
if(axis.remaining) {
|
||||
auto offset = axis.direction * (s32)(axis.remaining & 3);
|
||||
axis.phase = (axis.phase + 4 + offset) & 3;
|
||||
}
|
||||
axis.remaining = 0;
|
||||
axis.next = 0;
|
||||
axis.interval = 1;
|
||||
}
|
||||
|
||||
auto RelativePointingDevice::queue(AxisState& axis, s64 delta) -> void {
|
||||
if(delta < -32768) delta = -32768;
|
||||
if(delta > 32767) delta = 32767;
|
||||
|
||||
auto scaled = delta * scaleNumerator + axis.remainder;
|
||||
auto transitions = scaled < 0
|
||||
? (scaled - ScaleDenominator / 2) / ScaleDenominator
|
||||
: (scaled + ScaleDenominator / 2) / ScaleDenominator;
|
||||
axis.remainder = scaled - transitions * ScaleDenominator;
|
||||
if(transitions < -(s64)MaximumTransitions) transitions = -(s64)MaximumTransitions;
|
||||
if(transitions > (s64)MaximumTransitions) transitions = (s64)MaximumTransitions;
|
||||
if(!transitions) return;
|
||||
|
||||
axis.direction = transitions < 0 ? -1 : 1;
|
||||
axis.remaining = abs(transitions);
|
||||
axis.interval = max((u64)1, windowDuration() / axis.remaining);
|
||||
axis.next = axis.interval;
|
||||
}
|
||||
|
||||
auto RelativePointingDevice::advance(AxisState& axis, u64 timestamp) -> void {
|
||||
if(!axis.remaining || timestamp < axis.next) return;
|
||||
|
||||
auto due = 1 + (timestamp - axis.next) / axis.interval;
|
||||
if(due > axis.remaining) due = axis.remaining;
|
||||
auto offset = axis.direction * (s32)(due & 3);
|
||||
axis.phase = (axis.phase + 4 + offset) & 3;
|
||||
axis.remaining -= due;
|
||||
axis.next += axis.interval * due;
|
||||
}
|
||||
|
||||
auto RelativePointingDevice::windowDuration() const -> u64 {
|
||||
return cpu.scalar() * 228 * (Region::NTSC() ? 262 : 312);
|
||||
}
|
||||
|
||||
auto RelativePointingDevice::serialize(serializer& s) -> void {
|
||||
for(auto* axis : {&horizontal, &vertical}) {
|
||||
s(axis->phase);
|
||||
s(axis->direction);
|
||||
s(axis->remaining);
|
||||
s(axis->next);
|
||||
s(axis->interval);
|
||||
s(axis->remainder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
struct RelativePointingDevice : Controller {
|
||||
Node::Input::Axis x;
|
||||
Node::Input::Axis y;
|
||||
Node::Input::Button fire;
|
||||
|
||||
RelativePointingDevice(Node::Port, string name, u32 scaleNumerator);
|
||||
|
||||
auto poll() -> void override;
|
||||
auto read() -> n8 override;
|
||||
auto serialize(serializer&) -> void override;
|
||||
|
||||
protected:
|
||||
virtual auto encode(u8 xPhase, u8 yPhase, bool left, bool down) const -> u8 = 0;
|
||||
|
||||
private:
|
||||
struct AxisState {
|
||||
u8 phase = 0;
|
||||
s8 direction = 0;
|
||||
u32 remaining = 0;
|
||||
u64 next = 0;
|
||||
u64 interval = 1;
|
||||
s32 remainder = 0;
|
||||
} horizontal, vertical;
|
||||
|
||||
auto finish(AxisState&) -> void;
|
||||
auto queue(AxisState&, s64 delta) -> void;
|
||||
auto advance(AxisState&, u64 timestamp) -> void;
|
||||
auto windowDuration() const -> u64;
|
||||
|
||||
const u32 scaleNumerator;
|
||||
static constexpr u32 ScaleDenominator = 5;
|
||||
static constexpr u32 MaximumTransitions = 32767;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
SaveKey::SaveKey(Node::Port parent, string name, string filename) {
|
||||
node = parent->append<Node::Peripheral>(name);
|
||||
node->setPak(pak = platform->pak(node));
|
||||
|
||||
persistent.load(pak, filename, 32_KiB, 0xff);
|
||||
eeprom.load(M24C::Type::M24C256);
|
||||
synchronizeEeprom();
|
||||
eeprom.power();
|
||||
}
|
||||
|
||||
auto SaveKey::save() -> void {
|
||||
synchronizePersistent();
|
||||
persistent.flush(pak);
|
||||
}
|
||||
|
||||
auto SaveKey::power(bool reset) -> void {
|
||||
eeprom.power();
|
||||
}
|
||||
|
||||
auto SaveKey::read() -> n8 {
|
||||
n8 data = 0xff;
|
||||
data.bit(2) = eeprom.read();
|
||||
return data;
|
||||
}
|
||||
|
||||
auto SaveKey::write(n8 data) -> void {
|
||||
auto transactionBoundary = eeprom.clock() && data.bit(3) && eeprom.data() != data.bit(2);
|
||||
eeprom.clock = eeprom.clock();
|
||||
eeprom.data = eeprom.data();
|
||||
eeprom.clock = data.bit(3);
|
||||
eeprom.data = data.bit(2);
|
||||
eeprom.write();
|
||||
if(transactionBoundary) synchronizePersistent();
|
||||
}
|
||||
|
||||
auto SaveKey::serialize(serializer& s) -> void {
|
||||
if(s.writing()) synchronizePersistent();
|
||||
eeprom.serialize(s, false);
|
||||
if(s.reading()) synchronizeEeprom();
|
||||
}
|
||||
|
||||
auto SaveKey::synchronizeEeprom() -> void {
|
||||
std::copy(persistent.memory.begin(), persistent.memory.end(), std::begin(eeprom.memory));
|
||||
}
|
||||
|
||||
auto SaveKey::synchronizePersistent() -> void {
|
||||
persistent.replace({(const u8*)eeprom.memory, eeprom.size()});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
struct SaveKey : Controller {
|
||||
SaveKey(Node::Port, string name = "SaveKey", string filename = "savekey.eeprom");
|
||||
|
||||
auto save() -> void override;
|
||||
auto power(bool reset) -> void override;
|
||||
auto read() -> n8 override;
|
||||
auto write(n8 data) -> void override;
|
||||
auto serialize(serializer&) -> void override;
|
||||
|
||||
protected:
|
||||
auto synchronizeEeprom() -> void;
|
||||
auto synchronizePersistent() -> void;
|
||||
|
||||
VFS::Pak pak;
|
||||
M24C eeprom;
|
||||
PersistentMemory persistent;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
SegaGenesis::SegaGenesis(Node::Port parent) : Gamepad(parent, "Sega Genesis") {
|
||||
buttonC = node->append<Node::Input::Button>("C");
|
||||
}
|
||||
|
||||
auto SegaGenesis::readAnalog(n1 index) -> AnalogConnection {
|
||||
if(!index) return AnalogConnection::vcc();
|
||||
platform->input(buttonC);
|
||||
return buttonC->value() ? AnalogConnection::ground() : AnalogConnection::vcc();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
struct SegaGenesis : Gamepad {
|
||||
Node::Input::Button buttonC;
|
||||
|
||||
SegaGenesis(Node::Port);
|
||||
|
||||
auto readAnalog(n1 index) -> AnalogConnection override;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
TrakBall::TrakBall(Node::Port parent, string name)
|
||||
: RelativePointingDevice(parent, name, 2) {
|
||||
}
|
||||
|
||||
auto TrakBall::encode(u8 xPhase, u8 yPhase, bool left, bool down) const -> u8 {
|
||||
static constexpr u8 horizontal[2][2] = {
|
||||
{0b00, 0b01}, {0b10, 0b11},
|
||||
};
|
||||
static constexpr u8 vertical[2][2] = {
|
||||
{0b0100, 0b0000}, {0b1100, 0b1000},
|
||||
};
|
||||
return horizontal[xPhase & 1][left] | vertical[yPhase & 1][down];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
struct TrakBall : RelativePointingDevice {
|
||||
TrakBall(Node::Port, string name);
|
||||
|
||||
protected:
|
||||
auto encode(u8 xPhase, u8 yPhase, bool left, bool down) const -> u8 override;
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
XG1LightGun::XG1LightGun(Node::Port parent) {
|
||||
node = parent->append<Node::Peripheral>("XG-1 Light Gun");
|
||||
|
||||
x = node->append<Node::Input::Axis>("X");
|
||||
y = node->append<Node::Input::Axis>("Y");
|
||||
trigger = node->append<Node::Input::Button>("Trigger");
|
||||
|
||||
targetY = video.displayHeight() / 2;
|
||||
sprite = node->append<Node::Video::Sprite>("Crosshair");
|
||||
sprite->setImage(Resource::Sprite::Famicom::Crosshair);
|
||||
video.screen->attach(sprite);
|
||||
updateCrosshair();
|
||||
}
|
||||
|
||||
XG1LightGun::~XG1LightGun() {
|
||||
if(video.screen) video.screen->detach(sprite);
|
||||
}
|
||||
|
||||
auto XG1LightGun::poll() -> void {
|
||||
platform->input(x);
|
||||
platform->input(y);
|
||||
|
||||
targetX = std::clamp<s64>(targetX + x->value(), -TargetMargin, 160 + TargetMargin - 1);
|
||||
targetY = std::clamp<s64>(targetY + y->value(), -TargetMargin,
|
||||
video.displayHeight() + TargetMargin - 1);
|
||||
updateCrosshair();
|
||||
}
|
||||
|
||||
auto XG1LightGun::read() -> n8 {
|
||||
platform->input(trigger);
|
||||
|
||||
n8 data = 0xff;
|
||||
data.bit(0) = !trigger->value();
|
||||
data.bit(4) = !lightDetected();
|
||||
return data;
|
||||
}
|
||||
|
||||
auto XG1LightGun::serialize(serializer& s) -> void {
|
||||
s(targetX);
|
||||
s(targetY);
|
||||
if(s.reading()) updateCrosshair();
|
||||
}
|
||||
|
||||
auto XG1LightGun::lightDetected() const -> bool {
|
||||
auto beamX = tia.displayPosition() - 68 + HorizontalOffset;
|
||||
if(beamX < 0) beamX += HorizontalClocks;
|
||||
auto beamY = video.y() + VerticalOffset;
|
||||
auto deltaX = beamX - targetX;
|
||||
auto deltaY = beamY - targetY;
|
||||
return deltaX >= 0 && deltaX < DetectionWidth && deltaY >= 0;
|
||||
}
|
||||
|
||||
auto XG1LightGun::updateCrosshair() -> void {
|
||||
sprite->setPosition(targetX + VideoLeftMargin - CrosshairRadius, targetY - CrosshairRadius);
|
||||
sprite->setVisible(targetX >= 0 && targetX < 160 && targetY >= 0 && targetY < video.displayHeight());
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
struct XG1LightGun : Controller {
|
||||
Node::Input::Axis x;
|
||||
Node::Input::Axis y;
|
||||
Node::Input::Button trigger;
|
||||
Node::Video::Sprite sprite;
|
||||
|
||||
XG1LightGun(Node::Port);
|
||||
~XG1LightGun();
|
||||
|
||||
auto poll() -> void override;
|
||||
auto read() -> n8 override;
|
||||
auto serialize(serializer&) -> void override;
|
||||
|
||||
private:
|
||||
auto lightDetected() const -> bool;
|
||||
auto updateCrosshair() -> void;
|
||||
|
||||
i16 targetX = 80;
|
||||
i16 targetY = 114;
|
||||
|
||||
static constexpr s32 TargetMargin = 16;
|
||||
static constexpr s32 HorizontalClocks = 228;
|
||||
static constexpr s32 HorizontalOffset = -23;
|
||||
static constexpr s32 VerticalOffset = 1;
|
||||
static constexpr s32 DetectionWidth = 15;
|
||||
static constexpr s32 CrosshairRadius = 8;
|
||||
static constexpr s32 VideoLeftMargin = 10;
|
||||
};
|
||||
Loaded 100 of 2669 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user