mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
506 Commits
2021-03-23
...
2021-07-09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5810a1a98e | ||
|
|
a4c011e3c0 | ||
|
|
337fd15dc0 | ||
|
|
9bc94f4536 | ||
|
|
3f4cf35384 | ||
|
|
4dd7f2cc09 | ||
|
|
1b29cc34c4 | ||
|
|
53c3c1f5ab | ||
|
|
6225abd751 | ||
|
|
c6fcd9a1eb | ||
|
|
30fbb6ea53 | ||
|
|
0e49258546 | ||
|
|
264b8dfb28 | ||
|
|
6a15b8f695 | ||
|
|
5167d256cc | ||
|
|
16bd826491 | ||
|
|
55af8fa5d9 | ||
|
|
1ec8ff20af | ||
|
|
99a65d3297 | ||
|
|
94907b51aa | ||
|
|
0085265d13 | ||
|
|
8e0893bd42 | ||
|
|
704dc9bdcb | ||
|
|
7a673a2448 | ||
|
|
33e2a4b21c | ||
|
|
3e6b804896 | ||
|
|
e98165a657 | ||
|
|
2a7727d12b | ||
|
|
c20e8f4062 | ||
|
|
4ca9db7d49 | ||
|
|
4add48cffb | ||
|
|
adbfb009f8 | ||
|
|
43ceca8711 | ||
|
|
3ef28a4f03 | ||
|
|
adcd580d5b | ||
|
|
5715c9183f | ||
|
|
ceb62ac7f9 | ||
|
|
bda0756620 | ||
|
|
6b47fb38c6 | ||
|
|
38bf8a06a7 | ||
|
|
196651d9aa | ||
|
|
6b46212a4e | ||
|
|
2a6fff2008 | ||
|
|
c5944efe50 | ||
|
|
f384370b18 | ||
|
|
0c09275a9f | ||
|
|
278671cdb9 | ||
|
|
964d2d4fa4 | ||
|
|
f371221dba | ||
|
|
27b0579ec6 | ||
|
|
283092cfbc | ||
|
|
614953a222 | ||
|
|
4fffb3cf19 | ||
|
|
850aa2b23a | ||
|
|
d715e5fd1d | ||
|
|
7826a26c7b | ||
|
|
dc0a82cf9a | ||
|
|
2e60c81bd6 | ||
|
|
763b9ba0ec | ||
|
|
bae8bb0c00 | ||
|
|
bcf483fb7e | ||
|
|
a5b7d819a7 | ||
|
|
fe07a0b1d8 | ||
|
|
d9231e5d4a | ||
|
|
b7aa1a1c84 | ||
|
|
32e144115d | ||
|
|
177cc96f49 | ||
|
|
51d98ef9ab | ||
|
|
2327c48cc4 | ||
|
|
742d44a532 | ||
|
|
52b96db2b9 | ||
|
|
0b9de78c38 | ||
|
|
2c28cb8c57 | ||
|
|
483fe82e9d | ||
|
|
29492d6138 | ||
|
|
19310e32c4 | ||
|
|
c04a395499 | ||
|
|
1c424833a9 | ||
|
|
a46ff5590d | ||
|
|
ab059b63fd | ||
|
|
3d8fc9952d | ||
|
|
8ce8fbd977 | ||
|
|
7f08218b28 | ||
|
|
2c139ad931 | ||
|
|
1119779c8b | ||
|
|
5351ac560f | ||
|
|
49f0ab0f15 | ||
|
|
a5c57e777e | ||
|
|
3c59042388 | ||
|
|
919e211bc4 | ||
|
|
daa0737ce4 | ||
|
|
36805cb120 | ||
|
|
7de69e9874 | ||
|
|
b93575bbcc | ||
|
|
116e0f0105 | ||
|
|
e4a650aaff | ||
|
|
b5312b9ba0 | ||
|
|
6afee7bb9b | ||
|
|
5729e6e13a | ||
|
|
2f53b105bb | ||
|
|
b698056f78 | ||
|
|
95c906f03d | ||
|
|
be19fa9dde | ||
|
|
81e9ba5608 | ||
|
|
f2d7b9f6a9 | ||
|
|
1ea034310a | ||
|
|
bdcab447f9 | ||
|
|
10bf6744aa | ||
|
|
895d98e266 | ||
|
|
903e343895 | ||
|
|
f8b7c59616 | ||
|
|
fcd267a3f9 | ||
|
|
f8bb66d2a0 | ||
|
|
90782d3c27 | ||
|
|
f2336d2efc | ||
|
|
c2d093fa3c | ||
|
|
1a97cc8a91 | ||
|
|
c34a548fa0 | ||
|
|
d1b89392a2 | ||
|
|
ed734754e5 | ||
|
|
520c3c9218 | ||
|
|
9230cf1726 | ||
|
|
6e616972a5 | ||
|
|
f98888824d | ||
|
|
6c8b23e708 | ||
|
|
2c2bb3765f | ||
|
|
0d165740ea | ||
|
|
88f0f2b623 | ||
|
|
0afa143375 | ||
|
|
8319aca351 | ||
|
|
a66734883a | ||
|
|
d2ab0dd839 | ||
|
|
2574407afb | ||
|
|
83a54fd6d2 | ||
|
|
e062780968 | ||
|
|
3acd0be1f7 | ||
|
|
69c0734975 | ||
|
|
c1678d7be7 | ||
|
|
117f9a9794 | ||
|
|
b49cc407c6 | ||
|
|
954386f1cc | ||
|
|
d7ff6bd04d | ||
|
|
6025516f9f | ||
|
|
d8b9cdf7a2 | ||
|
|
09dbff39f2 | ||
|
|
2fe15a6168 | ||
|
|
07dc26f8fa | ||
|
|
a08d65b1ff | ||
|
|
199621db08 | ||
|
|
0e1e8c7faa | ||
|
|
42a98e1676 | ||
|
|
23e26e0333 | ||
|
|
fadb04f3f3 | ||
|
|
4968ccf46d | ||
|
|
1dcac304d3 | ||
|
|
1651efe4fc | ||
|
|
8f24aed43e | ||
|
|
a381374e31 | ||
|
|
9411c37d23 | ||
|
|
6af6f21868 | ||
|
|
9a0022cfcb | ||
|
|
266310d9c2 | ||
|
|
fbf1adef05 | ||
|
|
311ddfb05a | ||
|
|
2fd8a8aa66 | ||
|
|
0c3e9dca28 | ||
|
|
c331d15429 | ||
|
|
4414e96710 | ||
|
|
7161783a4f | ||
|
|
cbac48da86 | ||
|
|
d9142d5427 | ||
|
|
e5e988b28f | ||
|
|
e94e051c87 | ||
|
|
5fc91effb5 | ||
|
|
6c9dacbe89 | ||
|
|
6a7eb832cc | ||
|
|
60cf8486bb | ||
|
|
90b8163d54 | ||
|
|
a1e4389f63 | ||
|
|
440b11708b | ||
|
|
f90dce5c54 | ||
|
|
606c7709cf | ||
|
|
1d1e6d1820 | ||
|
|
3eb4dd74a2 | ||
|
|
853914480c | ||
|
|
fe04410681 | ||
|
|
1f686c4e6b | ||
|
|
2a2ac1227b | ||
|
|
b5340c8f74 | ||
|
|
196c4dcdd9 | ||
|
|
c5a86f0ef7 | ||
|
|
88f2a2940b | ||
|
|
26b019a4d4 | ||
|
|
5f7b3ae313 | ||
|
|
61c127ed2e | ||
|
|
333981e2a7 | ||
|
|
423fbc9ac7 | ||
|
|
1c1719e561 | ||
|
|
56c30e1651 | ||
|
|
1ea4130035 | ||
|
|
57713d63fa | ||
|
|
d18a537509 | ||
|
|
8e0a6df03b | ||
|
|
95a52a9f62 | ||
|
|
ae2993625c | ||
|
|
0982141442 | ||
|
|
85fab2abc4 | ||
|
|
de3b37799c | ||
|
|
70851f3b2d | ||
|
|
462bbf2e40 | ||
|
|
778b9ef683 | ||
|
|
96e7eb1bed | ||
|
|
05671f3553 | ||
|
|
6e4832f999 | ||
|
|
54e3332673 | ||
|
|
6c559d7556 | ||
|
|
9165a85484 | ||
|
|
98ada2588a | ||
|
|
43f686c22d | ||
|
|
4a2673d757 | ||
|
|
f27e331462 | ||
|
|
dd64aef910 | ||
|
|
95971f39f1 | ||
|
|
83beb3c0e6 | ||
|
|
76335e5cf2 | ||
|
|
4494320238 | ||
|
|
5acd97c860 | ||
|
|
b0f551c307 | ||
|
|
b6b3d845a3 | ||
|
|
505d84f336 | ||
|
|
1d5144b912 | ||
|
|
deff91e460 | ||
|
|
afd8dc0915 | ||
|
|
fbee74e1fe | ||
|
|
ccd82591aa | ||
|
|
64931e476d | ||
|
|
604a715a49 | ||
|
|
24757ef20c | ||
|
|
e36cc9e777 | ||
|
|
2e999889bd | ||
|
|
f4db4c3a73 | ||
|
|
d923fe72c0 | ||
|
|
f05cdd5e34 | ||
|
|
f9954619d4 | ||
|
|
0aa8c3c40d | ||
|
|
a30eeaab6a | ||
|
|
3858e79579 | ||
|
|
b4a5fa33b0 | ||
|
|
2a6e9c5e8a | ||
|
|
488c2aed51 | ||
|
|
5483f979dc | ||
|
|
ea11f3826a | ||
|
|
ceae81a332 | ||
|
|
50ea56e908 | ||
|
|
bfb2f79cff | ||
|
|
8268e8ee4c | ||
|
|
cb31e22f59 | ||
|
|
6752f4fd73 | ||
|
|
22c31e4f55 | ||
|
|
c2ff64c1e0 | ||
|
|
4db792591a | ||
|
|
1290a8e32b | ||
|
|
8ae38991b0 | ||
|
|
6d40549c0c | ||
|
|
93d5c9a3c7 | ||
|
|
9af6c0b37a | ||
|
|
7e3528c692 | ||
|
|
41f2fc51be | ||
|
|
11228dc265 | ||
|
|
ef50967793 | ||
|
|
5f6c08b7e0 | ||
|
|
6cb23ec5be | ||
|
|
1bae70bcf8 | ||
|
|
9820591ba4 | ||
|
|
77071b3c69 | ||
|
|
335e839b31 | ||
|
|
6fe947b8b9 | ||
|
|
22b29e77a7 | ||
|
|
4858cfce6b | ||
|
|
8da3e91f5e | ||
|
|
012235bfeb | ||
|
|
052e284c33 | ||
|
|
32e3dd71b1 | ||
|
|
95f4272919 | ||
|
|
00679b6135 | ||
|
|
2c18bb4508 | ||
|
|
0cf1c9040a | ||
|
|
9196341482 | ||
|
|
685140a4c2 | ||
|
|
1465b0ee4d | ||
|
|
0bf6b765d3 | ||
|
|
4774676e2a | ||
|
|
9c29655da2 | ||
|
|
c8ab18f2b6 | ||
|
|
8ebce466db | ||
|
|
1b39b17125 | ||
|
|
5a46853075 | ||
|
|
48ad4d4c4c | ||
|
|
056a036712 | ||
|
|
70eaa79108 | ||
|
|
20c814a4dd | ||
|
|
6a052e1900 | ||
|
|
cecdf8584a | ||
|
|
4758bc8615 | ||
|
|
c906dc3c0a | ||
|
|
d1dcb41b6f | ||
|
|
96ac86a757 | ||
|
|
4919786825 | ||
|
|
24b4185714 | ||
|
|
ad10d0037a | ||
|
|
b6554c8255 | ||
|
|
01dc83d0d6 | ||
|
|
2fd08789ab | ||
|
|
bc9e529995 | ||
|
|
708c24cc57 | ||
|
|
7fb3048257 | ||
|
|
9319f0525a | ||
|
|
b7a62e0121 | ||
|
|
bd5dd9b9a3 | ||
|
|
3348167c46 | ||
|
|
700c505974 | ||
|
|
d403036d86 | ||
|
|
5e08d7db39 | ||
|
|
c34cb310a8 | ||
|
|
8d86aa69bc | ||
|
|
cc41ccc5f1 | ||
|
|
e6252fe0ed | ||
|
|
03577de675 | ||
|
|
205518ba75 | ||
|
|
2510064218 | ||
|
|
0ef2806970 | ||
|
|
d80f03e369 | ||
|
|
fd271d920b | ||
|
|
2bbf8bc9fa | ||
|
|
9b65d56ed0 | ||
|
|
a5098a60ec | ||
|
|
0ebd900e40 | ||
|
|
7aeb17ac92 | ||
|
|
cc78bfb229 | ||
|
|
485c2a866c | ||
|
|
5b419ca5bf | ||
|
|
14ae579fca | ||
|
|
1c2ea0d7fe | ||
|
|
e7a9ae18a1 | ||
|
|
d61f478a39 | ||
|
|
9cc747b3e2 | ||
|
|
2f223f7db2 | ||
|
|
17f11a3be3 | ||
|
|
37dcf61130 | ||
|
|
856ebfacca | ||
|
|
9731fdd33b | ||
|
|
5ea605ccf7 | ||
|
|
d0c789ff9a | ||
|
|
9baa861742 | ||
|
|
30a1a53c97 | ||
|
|
bdb1b7e77c | ||
|
|
9293bcbc88 | ||
|
|
c481f475e7 | ||
|
|
ef01471e17 | ||
|
|
73c8157197 | ||
|
|
af1dc2d3b2 | ||
|
|
8f6b3feee1 | ||
|
|
a20f5528b7 | ||
|
|
f48876d80e | ||
|
|
db52f13c32 | ||
|
|
2590769d3f | ||
|
|
5667dcac36 | ||
|
|
bec71ead39 | ||
|
|
e4d9022d37 | ||
|
|
572be48f38 | ||
|
|
6f4ccebfa1 | ||
|
|
77fcf52d27 | ||
|
|
79c2bc1fd7 | ||
|
|
76370d9418 | ||
|
|
7bac18bd65 | ||
|
|
704737144a | ||
|
|
2a9c73a1d3 | ||
|
|
e87e851401 | ||
|
|
80d4846a27 | ||
|
|
9fd53c9c91 | ||
|
|
53eae873d8 | ||
|
|
93422f4b1c | ||
|
|
06cedb2e50 | ||
|
|
7fdb1d848b | ||
|
|
246fd9442f | ||
|
|
eb99a64b29 | ||
|
|
d7954a4cb1 | ||
|
|
ef636da866 | ||
|
|
fa18b06dbf | ||
|
|
349b9ce502 | ||
|
|
b2cf121410 | ||
|
|
71cf63bd35 | ||
|
|
d1bb3aada4 | ||
|
|
b4214c6e08 | ||
|
|
f5c7746493 | ||
|
|
f10ec80153 | ||
|
|
0af405aa46 | ||
|
|
cf481effa6 | ||
|
|
a1511f9600 | ||
|
|
325e2b3941 | ||
|
|
7017324d60 | ||
|
|
deb5d69ac7 | ||
|
|
68a04f4e6a | ||
|
|
0d61902b10 | ||
|
|
3eec210b30 | ||
|
|
5998f3b35b | ||
|
|
869567fdd9 | ||
|
|
2e70b5eb9f | ||
|
|
8a3bfb8672 | ||
|
|
06f1e64177 | ||
|
|
b42780173a | ||
|
|
36c8821c4c | ||
|
|
947de2d54a | ||
|
|
9347fe5f44 | ||
|
|
e82367def3 | ||
|
|
9cde7c12ba | ||
|
|
015556cc91 | ||
|
|
47c5a243aa | ||
|
|
070e359d82 | ||
|
|
b397059d5e | ||
|
|
400f54e508 | ||
|
|
e0736435f8 | ||
|
|
b09c5538c6 | ||
|
|
ce3d2913bf | ||
|
|
87202a2a27 | ||
|
|
818a4dff25 | ||
|
|
eacffa49f5 | ||
|
|
9e506c3206 | ||
|
|
29cf80339a | ||
|
|
50f53f7d97 | ||
|
|
73fbd89c85 | ||
|
|
f74fa06f2d | ||
|
|
ee989ab762 | ||
|
|
818655a9b6 | ||
|
|
57a7e0834f | ||
|
|
cd787486d2 | ||
|
|
67fd6787a6 | ||
|
|
627b96f73c | ||
|
|
8a6985c2e8 | ||
|
|
60e8273de2 | ||
|
|
aa8ce5c1ac | ||
|
|
dd28246f9f | ||
|
|
dc25a60b9b | ||
|
|
094d623485 | ||
|
|
1266bbb224 | ||
|
|
bd1ea5740a | ||
|
|
3e04b51122 | ||
|
|
76f2aba51a | ||
|
|
fd88071c0a | ||
|
|
16bfe1a55c | ||
|
|
90c3d6a1e8 | ||
|
|
18d6197d6c | ||
|
|
27eddf6dff | ||
|
|
57b32d9537 | ||
|
|
837b9499d5 | ||
|
|
c2fde2b147 | ||
|
|
f26bf4b9e4 | ||
|
|
1da51bee6c | ||
|
|
5a66956221 | ||
|
|
91d973c4a9 | ||
|
|
fa79589db8 | ||
|
|
e52649f74d | ||
|
|
d77ddaf4fa | ||
|
|
9ff392279a | ||
|
|
448d9dc3e1 | ||
|
|
afb4e6d37d | ||
|
|
158122fbf4 | ||
|
|
417ece2386 | ||
|
|
77b241af4f | ||
|
|
25b8c4c062 | ||
|
|
1be88a5308 | ||
|
|
294280a94e | ||
|
|
32aebfebe0 | ||
|
|
14663bd06b | ||
|
|
68abd197aa | ||
|
|
18fd21eae7 | ||
|
|
3296347370 | ||
|
|
28c9463e0d | ||
|
|
044ac949ba | ||
|
|
87317f5673 | ||
|
|
5e21a49841 | ||
|
|
687c05365e | ||
|
|
4f80523828 | ||
|
|
76299a2add | ||
|
|
48f794dc2d | ||
|
|
51b8dcd011 | ||
|
|
acdbd88b9e | ||
|
|
00a3a3c724 | ||
|
|
729edeac6c | ||
|
|
faaa4961ed | ||
|
|
7937cc2d0f | ||
|
|
8a11a5832c | ||
|
|
53ba0e67d1 | ||
|
|
e8825aeada | ||
|
|
e90e30e766 | ||
|
|
9f6bb325e6 | ||
|
|
6e2c65435a | ||
|
|
052ab44f1c | ||
|
|
daa5679241 | ||
|
|
e055668554 | ||
|
|
c96829c29e | ||
|
|
c88abed2dc | ||
|
|
e42b6cb3c8 | ||
|
|
465ecc4a78 | ||
|
|
ae4ccdf5e6 | ||
|
|
fbe479c43f |
@@ -19,6 +19,7 @@ enum class Machine {
|
||||
AtariST,
|
||||
ColecoVision,
|
||||
Electron,
|
||||
Enterprise,
|
||||
Macintosh,
|
||||
MasterSystem,
|
||||
MSX,
|
||||
|
||||
81
Analyser/Static/Enterprise/StaticAnalyser.cpp
Normal file
81
Analyser/Static/Enterprise/StaticAnalyser.cpp
Normal file
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// StaticAnalyser.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 24/06/2021.
|
||||
// Copyright 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "StaticAnalyser.hpp"
|
||||
#include "Target.hpp"
|
||||
|
||||
#include "../../../Storage/Disk/Parsers/FAT.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
bool insensitive_equal(const std::string &lhs, const std::string &rhs) {
|
||||
return std::equal(
|
||||
lhs.begin(), lhs.end(),
|
||||
rhs.begin(), rhs.end(),
|
||||
[] (char l, char r) {
|
||||
return tolower(l) == tolower(r);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Analyser::Static::TargetList Analyser::Static::Enterprise::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) {
|
||||
// This analyser can comprehend disks only.
|
||||
if(media.disks.empty()) return {};
|
||||
|
||||
// Otherwise, assume a return will happen.
|
||||
Analyser::Static::TargetList targets;
|
||||
using Target = Analyser::Static::Enterprise::Target;
|
||||
auto *const target = new Target;
|
||||
target->media = media;
|
||||
|
||||
// Always require a BASIC.
|
||||
target->basic_version = Target::BASICVersion::Any;
|
||||
|
||||
// Inspect any supplied disks.
|
||||
if(!media.disks.empty()) {
|
||||
// DOS will be needed.
|
||||
target->dos = Target::DOS::EXDOS;
|
||||
|
||||
// Grab the volume information, which includes the root directory.
|
||||
auto volume = Storage::Disk::FAT::GetVolume(media.disks.front());
|
||||
if(volume) {
|
||||
// If there's an EXDOS.INI then this disk should be able to boot itself.
|
||||
// If not but if there's only one .COM or .BAS, automatically load that.
|
||||
// Failing that, issue a :DIR and give the user a clue as to how to load.
|
||||
const Storage::Disk::FAT::File *selected_file = nullptr;
|
||||
bool has_exdos_ini = false;
|
||||
bool did_pick_file = false;
|
||||
for(const auto &file: (*volume).root_directory) {
|
||||
if(insensitive_equal(file.name, "exdos") && insensitive_equal(file.extension, "ini")) {
|
||||
has_exdos_ini = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if(insensitive_equal(file.extension, "com") || insensitive_equal(file.extension, "bas")) {
|
||||
did_pick_file = !selected_file;
|
||||
selected_file = &file;
|
||||
}
|
||||
}
|
||||
|
||||
if(!has_exdos_ini) {
|
||||
if(did_pick_file) {
|
||||
target->loading_command = std::string("run \"") + selected_file->name + "." + selected_file->extension + "\"\n";
|
||||
} else {
|
||||
target->loading_command = ":dir\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
targets.push_back(std::unique_ptr<Analyser::Static::Target>(target));
|
||||
|
||||
return targets;
|
||||
}
|
||||
27
Analyser/Static/Enterprise/StaticAnalyser.hpp
Normal file
27
Analyser/Static/Enterprise/StaticAnalyser.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// StaticAnalyser.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 24/06/2021.
|
||||
// Copyright 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_Enterprise_StaticAnalyser_hpp
|
||||
#define Analyser_Static_Enterprise_StaticAnalyser_hpp
|
||||
|
||||
#include "../StaticAnalyser.hpp"
|
||||
#include "../../../Storage/TargetPlatforms.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Enterprise {
|
||||
|
||||
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif /* Analyser_Static_Enterprise_StaticAnalyser_hpp */
|
||||
57
Analyser/Static/Enterprise/Target.hpp
Normal file
57
Analyser/Static/Enterprise/Target.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// Target.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Analyser_Static_Enterprise_Target_h
|
||||
#define Analyser_Static_Enterprise_Target_h
|
||||
|
||||
#include "../../../Reflection/Enum.hpp"
|
||||
#include "../../../Reflection/Struct.hpp"
|
||||
#include "../StaticAnalyser.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
namespace Enterprise {
|
||||
|
||||
struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Target> {
|
||||
ReflectableEnum(Model, Enterprise64, Enterprise128, Enterprise256);
|
||||
ReflectableEnum(EXOSVersion, v10, v20, v21, v23, Any);
|
||||
ReflectableEnum(BASICVersion, v10, v11, v21, Any, None);
|
||||
ReflectableEnum(DOS, EXDOS, None);
|
||||
ReflectableEnum(Speed, FourMHz, SixMHz);
|
||||
|
||||
Model model = Model::Enterprise128;
|
||||
EXOSVersion exos_version = EXOSVersion::Any;
|
||||
BASICVersion basic_version = BASICVersion::None;
|
||||
DOS dos = DOS::None;
|
||||
Speed speed = Speed::FourMHz;
|
||||
std::string loading_command;
|
||||
|
||||
Target() : Analyser::Static::Target(Machine::Enterprise) {
|
||||
if(needs_declare()) {
|
||||
AnnounceEnum(Model);
|
||||
AnnounceEnum(EXOSVersion);
|
||||
AnnounceEnum(BASICVersion);
|
||||
AnnounceEnum(DOS);
|
||||
AnnounceEnum(Speed);
|
||||
|
||||
DeclareField(model);
|
||||
DeclareField(exos_version);
|
||||
DeclareField(basic_version);
|
||||
DeclareField(dos);
|
||||
DeclareField(speed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Analyser_Static_Enterprise_Target_h */
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "Coleco/StaticAnalyser.hpp"
|
||||
#include "Commodore/StaticAnalyser.hpp"
|
||||
#include "DiskII/StaticAnalyser.hpp"
|
||||
#include "Enterprise/StaticAnalyser.hpp"
|
||||
#include "Macintosh/StaticAnalyser.hpp"
|
||||
#include "MSX/StaticAnalyser.hpp"
|
||||
#include "Oric/StaticAnalyser.hpp"
|
||||
@@ -43,9 +44,9 @@
|
||||
#include "../../Storage/Disk/DiskImage/Formats/MacintoshIMG.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/G64.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/DMK.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/FAT12.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/HFE.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/MSA.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/MSXDSK.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/NIB.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp"
|
||||
#include "../../Storage/Disk/DiskImage/Formats/SSD.hpp"
|
||||
@@ -55,8 +56,14 @@
|
||||
|
||||
// Mass Storage Devices (i.e. usually, hard disks)
|
||||
#include "../../Storage/MassStorage/Formats/DAT.hpp"
|
||||
#include "../../Storage/MassStorage/Formats/DSK.hpp"
|
||||
#include "../../Storage/MassStorage/Formats/HFV.hpp"
|
||||
|
||||
// State Snapshots
|
||||
#include "../../Storage/State/SNA.hpp"
|
||||
#include "../../Storage/State/SZX.hpp"
|
||||
#include "../../Storage/State/Z80.hpp"
|
||||
|
||||
// Tapes
|
||||
#include "../../Storage/Tape/Formats/CAS.hpp"
|
||||
#include "../../Storage/Tape/Formats/CommodoreTAP.hpp"
|
||||
@@ -73,15 +80,23 @@
|
||||
|
||||
using namespace Analyser::Static;
|
||||
|
||||
static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::IntType &potential_platforms) {
|
||||
Media result;
|
||||
namespace {
|
||||
|
||||
std::string get_extension(const std::string &name) {
|
||||
// Get the extension, if any; it will be assumed that extensions are reliable, so an extension is a broad-phase
|
||||
// test as to file format.
|
||||
std::string::size_type final_dot = file_name.find_last_of(".");
|
||||
if(final_dot == std::string::npos) return result;
|
||||
std::string extension = file_name.substr(final_dot + 1);
|
||||
std::string::size_type final_dot = name.find_last_of(".");
|
||||
if(final_dot == std::string::npos) return name;
|
||||
std::string extension = name.substr(final_dot + 1);
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
|
||||
return extension;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::IntType &potential_platforms) {
|
||||
Media result;
|
||||
const std::string extension = get_extension(file_name);
|
||||
|
||||
#define InsertInstance(list, instance, platforms) \
|
||||
list.emplace_back(instance);\
|
||||
@@ -131,8 +146,9 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
TargetPlatform::AmstradCPC | TargetPlatform::Oric | TargetPlatform::ZXSpectrum) // DSK (Amstrad CPC, etc)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DSK (Apple II)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // DSK (Macintosh, floppy disk)
|
||||
Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MSXDSK>, TargetPlatform::MSX) // DSK (MSX)
|
||||
Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk, single volume image)
|
||||
Format("dsk", result.mass_storage_devices, MassStorage::DSK, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk, full device image)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::FAT12>, TargetPlatform::MSX) // DSK (MSX)
|
||||
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric) // DSK (Oric)
|
||||
Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore) // G64
|
||||
Format( "hfe",
|
||||
@@ -142,6 +158,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
// HFE (TODO: switch to AllDisk once the MSX stops being so greedy)
|
||||
Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
|
||||
Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
|
||||
Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::FAT12>, TargetPlatform::Enterprise) // IMG (Enterprise/MS-DOS style)
|
||||
Format("msa", result.disks, Disk::DiskImageHolder<Storage::Disk::MSA>, TargetPlatform::AtariST) // MSA
|
||||
Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII) // NIB
|
||||
Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // O
|
||||
@@ -199,14 +216,35 @@ Media Analyser::Static::GetMedia(const std::string &file_name) {
|
||||
|
||||
TargetList Analyser::Static::GetTargets(const std::string &file_name) {
|
||||
TargetList targets;
|
||||
const std::string extension = get_extension(file_name);
|
||||
|
||||
// Check whether the file directly identifies a target; if so then just return that.
|
||||
#define Format(ext, class) \
|
||||
if(extension == ext) { \
|
||||
try { \
|
||||
auto target = Storage::State::class::load(file_name); \
|
||||
if(target) { \
|
||||
targets.push_back(std::move(target)); \
|
||||
return targets; \
|
||||
} \
|
||||
} catch(...) {} \
|
||||
}
|
||||
|
||||
Format("sna", SNA);
|
||||
Format("szx", SZX);
|
||||
Format("z80", Z80);
|
||||
|
||||
#undef TryInsert
|
||||
|
||||
// Otherwise:
|
||||
//
|
||||
// Collect all disks, tapes ROMs, etc as can be extrapolated from this file, forming the
|
||||
// union of all platforms this file might be a target for.
|
||||
TargetPlatform::IntType potential_platforms = 0;
|
||||
Media media = GetMediaAndPlatforms(file_name, potential_platforms);
|
||||
|
||||
// Hand off to platform-specific determination of whether these things are actually compatible and,
|
||||
// if so, how to load them.
|
||||
// Hand off to platform-specific determination of whether these
|
||||
// things are actually compatible and, if so, how to load them.
|
||||
#define Append(x) if(potential_platforms & TargetPlatform::x) {\
|
||||
auto new_targets = x::GetTargets(media, file_name, potential_platforms);\
|
||||
std::move(new_targets.begin(), new_targets.end(), std::back_inserter(targets));\
|
||||
@@ -220,6 +258,7 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) {
|
||||
Append(Coleco);
|
||||
Append(Commodore);
|
||||
Append(DiskII);
|
||||
Append(Enterprise);
|
||||
Append(Macintosh);
|
||||
Append(MSX);
|
||||
Append(Oric);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "../../Storage/Disk/Disk.hpp"
|
||||
#include "../../Storage/MassStorage/MassStorageDevice.hpp"
|
||||
#include "../../Storage/Tape/Tape.hpp"
|
||||
#include "../../Reflection/Struct.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -23,8 +24,10 @@
|
||||
namespace Analyser {
|
||||
namespace Static {
|
||||
|
||||
struct State;
|
||||
|
||||
/*!
|
||||
A list of disks, tapes and cartridges.
|
||||
A list of disks, tapes and cartridges, and possibly a state snapshot.
|
||||
*/
|
||||
struct Media {
|
||||
std::vector<std::shared_ptr<Storage::Disk::Disk>> disks;
|
||||
@@ -48,13 +51,16 @@ struct Media {
|
||||
};
|
||||
|
||||
/*!
|
||||
A list of disks, tapes and cartridges plus information about the machine to which to attach them and its configuration,
|
||||
and instructions on how to launch the software attached, plus a measure of confidence in this target's correctness.
|
||||
Describes a machine and possibly its state; conventionally subclassed to add other machine-specific configuration fields and any
|
||||
necessary instructions on how to launch any software provided, plus a measure of confidence in this target's correctness.
|
||||
*/
|
||||
struct Target {
|
||||
Target(Machine machine) : machine(machine) {}
|
||||
virtual ~Target() {}
|
||||
|
||||
// This field is entirely optional.
|
||||
std::unique_ptr<Reflection::Struct> state;
|
||||
|
||||
Machine machine;
|
||||
Media media;
|
||||
float confidence = 0.0f;
|
||||
|
||||
@@ -19,11 +19,15 @@ namespace ZXSpectrum {
|
||||
|
||||
struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> {
|
||||
ReflectableEnum(Model,
|
||||
SixteenK,
|
||||
FortyEightK,
|
||||
OneTwoEightK,
|
||||
Plus2,
|
||||
Plus2a,
|
||||
Plus3,
|
||||
);
|
||||
|
||||
Model model = Model::Plus2a;
|
||||
Model model = Model::Plus2;
|
||||
bool should_hold_enter = false;
|
||||
|
||||
Target(): Analyser::Static::Target(Machine::ZXSpectrum) {
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#define ClockReceiver_hpp
|
||||
|
||||
#include "ForceInline.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
/*
|
||||
Informal pattern for all classes that run from a clock cycle:
|
||||
@@ -176,6 +178,9 @@ class Cycles: public WrappedInt<Cycles> {
|
||||
public:
|
||||
forceinline constexpr Cycles(IntType l) noexcept : WrappedInt<Cycles>(l) {}
|
||||
forceinline constexpr Cycles() noexcept : WrappedInt<Cycles>() {}
|
||||
forceinline static constexpr Cycles max() {
|
||||
return Cycles(std::numeric_limits<IntType>::max());
|
||||
}
|
||||
|
||||
private:
|
||||
friend WrappedInt;
|
||||
@@ -195,6 +200,9 @@ class HalfCycles: public WrappedInt<HalfCycles> {
|
||||
public:
|
||||
forceinline constexpr HalfCycles(IntType l) noexcept : WrappedInt<HalfCycles>(l) {}
|
||||
forceinline constexpr HalfCycles() noexcept : WrappedInt<HalfCycles>() {}
|
||||
forceinline static constexpr HalfCycles max() {
|
||||
return HalfCycles(std::numeric_limits<IntType>::max());
|
||||
}
|
||||
|
||||
forceinline constexpr HalfCycles(const Cycles &cycles) noexcept : WrappedInt<HalfCycles>(cycles.as_integral() * 2) {}
|
||||
|
||||
@@ -205,7 +213,7 @@ class HalfCycles: public WrappedInt<HalfCycles> {
|
||||
|
||||
/*!
|
||||
Severs from @c this the effect of dividing by @c divisor; @c this will end up with
|
||||
the value of @c this modulo @c divisor and @c divided by @c divisor is returned.
|
||||
the value of @c this modulo @c divisor . @c this divided by @c divisor is returned.
|
||||
*/
|
||||
forceinline Cycles divide_cycles(const Cycles &divisor) {
|
||||
const HalfCycles half_divisor = HalfCycles(divisor);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#define JustInTime_h
|
||||
|
||||
#include "../Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "ClockingHintSource.hpp"
|
||||
#include "ForceInline.hpp"
|
||||
|
||||
/*!
|
||||
@@ -24,29 +25,69 @@
|
||||
|
||||
If the held object implements get_next_sequence_point() then it'll be used to flush implicitly
|
||||
as and when sequence points are hit. Callers can use will_flush() to predict these.
|
||||
|
||||
If the held object is a subclass of ClockingHint::Source, this template will register as an
|
||||
observer and potentially stop clocking or stop delaying clocking until just-in-time references
|
||||
as directed.
|
||||
|
||||
TODO: incorporate and codify AsyncJustInTimeActor.
|
||||
*/
|
||||
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class JustInTimeActor {
|
||||
template <class T, class LocalTimeScale = HalfCycles, int multiplier = 1, int divider = 1> class JustInTimeActor:
|
||||
public ClockingHint::Observer {
|
||||
private:
|
||||
/*!
|
||||
A std::unique_ptr deleter which causes an update_sequence_point to occur on the actor supplied
|
||||
to it at construction if it implements get_next_sequence_point(). Otherwise destruction is a no-op.
|
||||
|
||||
**Does not delete the object.**
|
||||
|
||||
This is used by the -> operators below, which provide a unique pointer to the enclosed object and
|
||||
update their sequence points upon its destruction — i.e. after the caller has made whatever call
|
||||
or calls as were relevant to the enclosed object.
|
||||
*/
|
||||
class SequencePointAwareDeleter {
|
||||
public:
|
||||
explicit SequencePointAwareDeleter(JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *actor) : actor_(actor) {}
|
||||
explicit SequencePointAwareDeleter(JustInTimeActor<T, LocalTimeScale, multiplier, divider> *actor) noexcept
|
||||
: actor_(actor) {}
|
||||
|
||||
void operator ()(const T *const) const {
|
||||
forceinline void operator ()(const T *const) const {
|
||||
if constexpr (has_sequence_points<T>::value) {
|
||||
actor_->update_sequence_point();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *const actor_;
|
||||
JustInTimeActor<T, LocalTimeScale, multiplier, divider> *const actor_;
|
||||
};
|
||||
|
||||
// This block of SFINAE determines whether objects of type T accepts Cycles or HalfCycles.
|
||||
using HalfRunFor = void (T::*const)(HalfCycles);
|
||||
static uint8_t half_sig(...);
|
||||
static uint16_t half_sig(HalfRunFor);
|
||||
using TargetTimeScale =
|
||||
std::conditional_t<
|
||||
sizeof(half_sig(&T::run_for)) == sizeof(uint16_t),
|
||||
HalfCycles,
|
||||
Cycles>;
|
||||
|
||||
public:
|
||||
/// Constructs a new JustInTimeActor using the same construction arguments as the included object.
|
||||
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
||||
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {
|
||||
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
|
||||
object_.set_clocking_hint_observer(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds time to the actor.
|
||||
forceinline void operator += (LocalTimeScale rhs) {
|
||||
///
|
||||
/// @returns @c true if adding time caused a flush; @c false otherwise.
|
||||
forceinline bool operator += (LocalTimeScale rhs) {
|
||||
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
|
||||
if(clocking_preference_ == ClockingHint::Preference::None) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (multiplier != 1) {
|
||||
time_since_update_ += rhs * multiplier;
|
||||
} else {
|
||||
@@ -54,20 +95,31 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
}
|
||||
is_flushed_ = false;
|
||||
|
||||
if constexpr (has_sequence_points<T>::value) {
|
||||
time_until_event_ -= rhs;
|
||||
if(time_until_event_ <= LocalTimeScale(0)) {
|
||||
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
|
||||
if (clocking_preference_ == ClockingHint::Preference::RealTime) {
|
||||
flush();
|
||||
update_sequence_point();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (has_sequence_points<T>::value) {
|
||||
time_until_event_ -= rhs * multiplier;
|
||||
if(time_until_event_ <= LocalTimeScale(0)) {
|
||||
time_overrun_ = time_until_event_ / divider;
|
||||
flush();
|
||||
update_sequence_point();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time and returns a pointer to the included object.
|
||||
///
|
||||
/// If this object provides sequence points, checks for changes to the next
|
||||
/// sequence point upon deletion of the pointer.
|
||||
forceinline auto operator->() {
|
||||
[[nodiscard]] forceinline auto operator->() {
|
||||
flush();
|
||||
return std::unique_ptr<T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(this));
|
||||
}
|
||||
@@ -75,26 +127,39 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
/// Acts exactly as per the standard ->, but preserves constness.
|
||||
///
|
||||
/// Despite being const, this will flush the object and, if relevant, update the next sequence point.
|
||||
forceinline auto operator -> () const {
|
||||
auto non_const_this = const_cast<JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *>(this);
|
||||
[[nodiscard]] forceinline auto operator -> () const {
|
||||
auto non_const_this = const_cast<JustInTimeActor<T, LocalTimeScale, multiplier, divider> *>(this);
|
||||
non_const_this->flush();
|
||||
return std::unique_ptr<const T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(non_const_this));
|
||||
}
|
||||
|
||||
/// @returns a pointer to the included object, without flushing time.
|
||||
forceinline T *last_valid() {
|
||||
[[nodiscard]] forceinline T *last_valid() {
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// @returns a const pointer to the included object, without flushing time.
|
||||
[[nodiscard]] forceinline const T *last_valid() const {
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// @returns the amount of time since the object was last flushed, in the target time scale.
|
||||
forceinline TargetTimeScale time_since_flush() const {
|
||||
// TODO: does this handle conversions properly where TargetTimeScale != LocalTimeScale?
|
||||
[[nodiscard]] forceinline TargetTimeScale time_since_flush() const {
|
||||
if constexpr (divider == 1) {
|
||||
return time_since_update_;
|
||||
}
|
||||
return TargetTimeScale(time_since_update_.as_integral() / divider);
|
||||
}
|
||||
|
||||
/// @returns the amount of time since the object was last flushed, plus the local time scale @c offset,
|
||||
/// converted to the target time scale.
|
||||
[[nodiscard]] forceinline TargetTimeScale time_since_flush(LocalTimeScale offset) const {
|
||||
if constexpr (divider == 1) {
|
||||
return time_since_update_ + offset;
|
||||
}
|
||||
return TargetTimeScale((time_since_update_ + offset).as_integral() / divider);
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time.
|
||||
///
|
||||
/// This does not affect this actor's record of when the next sequence point will occur.
|
||||
@@ -113,88 +178,95 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
}
|
||||
|
||||
/// Indicates whether a flush has occurred since the last call to did_flush().
|
||||
forceinline bool did_flush() {
|
||||
[[nodiscard]] forceinline bool did_flush() {
|
||||
const bool did_flush = did_flush_;
|
||||
did_flush_ = false;
|
||||
return did_flush;
|
||||
}
|
||||
|
||||
/// @returns a number in the range [-max, 0] indicating the offset of the most recent sequence
|
||||
/// point from the final time at the end of the += that triggered the sequence point.
|
||||
[[nodiscard]] forceinline LocalTimeScale last_sequence_point_overrun() {
|
||||
return time_overrun_;
|
||||
}
|
||||
|
||||
/// @returns the number of cycles until the next sequence-point-based flush, if the embedded object
|
||||
/// supports sequence points; @c LocalTimeScale() otherwise.
|
||||
LocalTimeScale cycles_until_implicit_flush() const {
|
||||
return time_until_event_;
|
||||
[[nodiscard]] LocalTimeScale cycles_until_implicit_flush() const {
|
||||
return time_until_event_ / divider;
|
||||
}
|
||||
|
||||
/// Indicates whether a sequence-point-caused flush will occur if the specified period is added.
|
||||
forceinline bool will_flush(LocalTimeScale rhs) const {
|
||||
[[nodiscard]] forceinline bool will_flush(LocalTimeScale rhs) const {
|
||||
if constexpr (!has_sequence_points<T>::value) {
|
||||
return false;
|
||||
}
|
||||
return rhs >= time_until_event_;
|
||||
}
|
||||
|
||||
/// Indicates the amount of time, in the local time scale, until the first local slot that falls wholly
|
||||
/// after @c duration, if that delay were to occur in @c offset units of time from now.
|
||||
[[nodiscard]] forceinline LocalTimeScale back_map(TargetTimeScale duration, TargetTimeScale offset) const {
|
||||
// A 1:1 mapping is easy.
|
||||
if constexpr (multiplier == 1 && divider == 1) {
|
||||
return duration;
|
||||
}
|
||||
|
||||
// Work out when this query is placed, and the time to which it relates
|
||||
const auto base = time_since_update_ + offset * divider;
|
||||
const auto target = base + duration * divider;
|
||||
|
||||
// Figure out the number of whole input steps that is required to get
|
||||
// past target, and subtract the number of whole input steps necessary
|
||||
// to get to base.
|
||||
const auto steps_to_base = base.as_integral() / multiplier;
|
||||
const auto steps_to_target = (target.as_integral() + divider - 1) / multiplier;
|
||||
|
||||
return LocalTimeScale(steps_to_target - steps_to_base);
|
||||
}
|
||||
|
||||
/// Updates this template's record of the next sequence point.
|
||||
void update_sequence_point() {
|
||||
if constexpr (has_sequence_points<T>::value) {
|
||||
time_until_event_ = object_.get_next_sequence_point();
|
||||
// Keep a fast path where no conversions will be applied; if conversions are
|
||||
// going to be applied then do a direct max -> max translation rather than
|
||||
// allowing the arithmetic to overflow.
|
||||
if constexpr (divider == 1 && std::is_same_v<LocalTimeScale, TargetTimeScale>) {
|
||||
time_until_event_ = object_.get_next_sequence_point();
|
||||
} else {
|
||||
const auto time = object_.get_next_sequence_point();
|
||||
if(time == TargetTimeScale::max()) {
|
||||
time_until_event_ = LocalTimeScale::max();
|
||||
} else {
|
||||
time_until_event_ = time * divider;
|
||||
}
|
||||
}
|
||||
assert(time_until_event_ > LocalTimeScale(0));
|
||||
}
|
||||
}
|
||||
|
||||
/// @returns A cached copy of the object's clocking preference.
|
||||
ClockingHint::Preference clocking_preference() const {
|
||||
return clocking_preference_;
|
||||
}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale time_since_update_, time_until_event_;
|
||||
LocalTimeScale time_since_update_, time_until_event_, time_overrun_;
|
||||
bool is_flushed_ = true;
|
||||
bool did_flush_ = false;
|
||||
|
||||
template <typename S, typename = void> struct has_sequence_points : std::false_type {};
|
||||
template <typename S> struct has_sequence_points<S, decltype(void(std::declval<S &>().get_next_sequence_point()))> : std::true_type {};
|
||||
};
|
||||
|
||||
/*!
|
||||
A RealTimeActor presents the same interface as a JustInTimeActor but doesn't defer work.
|
||||
Time added will be performed immediately.
|
||||
|
||||
Its primary purpose is to allow consumers to remain flexible in their scheduling.
|
||||
*/
|
||||
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class RealTimeActor {
|
||||
public:
|
||||
template<typename... Args> RealTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
||||
|
||||
forceinline void operator += (const LocalTimeScale &rhs) {
|
||||
if constexpr (multiplier == 1 && divider == 1) {
|
||||
object_.run_for(TargetTimeScale(rhs));
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (multiplier == 1) {
|
||||
accumulated_time_ += rhs;
|
||||
} else {
|
||||
accumulated_time_ += rhs * multiplier;
|
||||
}
|
||||
|
||||
if constexpr (divider == 1) {
|
||||
const auto duration = accumulated_time_.template flush<TargetTimeScale>();
|
||||
object_.run_for(duration);
|
||||
} else {
|
||||
const auto duration = accumulated_time_.template divide<TargetTimeScale>(LocalTimeScale(divider));
|
||||
if(duration > TargetTimeScale(0))
|
||||
object_.run_for(duration);
|
||||
}
|
||||
ClockingHint::Preference clocking_preference_ = ClockingHint::Preference::JustInTime;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference clocking) {
|
||||
clocking_preference_ = clocking;
|
||||
}
|
||||
|
||||
forceinline T *operator->() { return &object_; }
|
||||
forceinline const T *operator->() const { return &object_; }
|
||||
forceinline T *last_valid() { return &object_; }
|
||||
forceinline void flush() {}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale accumulated_time_;
|
||||
};
|
||||
|
||||
/*!
|
||||
A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
|
||||
An AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
|
||||
Any time the amount of accumulated time crosses a threshold provided at construction time,
|
||||
the object will be updated on the AsyncTaskQueue.
|
||||
*/
|
||||
|
||||
@@ -276,7 +276,10 @@ void WD1770::posit_event(int new_event_type) {
|
||||
goto test_type1_type;
|
||||
|
||||
begin_type1_spin_up:
|
||||
if((command_&0x08) || get_drive().get_motor_on()) goto test_type1_type;
|
||||
if((command_&0x08) || get_drive().get_motor_on()) {
|
||||
set_motor_on(true);
|
||||
goto test_type1_type;
|
||||
}
|
||||
SPIN_UP();
|
||||
|
||||
test_type1_type:
|
||||
@@ -387,7 +390,10 @@ void WD1770::posit_event(int new_event_type) {
|
||||
distance_into_section_ = 0;
|
||||
|
||||
if((command_&0x08) && has_motor_on_line()) goto test_type2_delay;
|
||||
if(!has_motor_on_line() && !has_head_load_line()) goto test_type2_delay;
|
||||
if(!has_motor_on_line() && !has_head_load_line()) {
|
||||
if(has_motor_on_line()) set_motor_on(true);
|
||||
goto test_type2_delay;
|
||||
}
|
||||
|
||||
if(has_motor_on_line()) goto begin_type2_spin_up;
|
||||
goto begin_type2_load_head;
|
||||
|
||||
@@ -129,8 +129,8 @@ ClockingHint::Preference ACIA::preferred_clocking() const {
|
||||
// because it's unclear when the interrupt might come.
|
||||
if(bits_incoming_ && receive_interrupt_enabled_) return ClockingHint::Preference::RealTime;
|
||||
|
||||
// No clocking required then.
|
||||
return ClockingHint::Preference::None;
|
||||
// Real-time clocking not required then.
|
||||
return ClockingHint::Preference::JustInTime;
|
||||
}
|
||||
|
||||
bool ACIA::get_interrupt_line() const {
|
||||
|
||||
@@ -208,7 +208,7 @@ void MFP68901::run_for(HalfCycles time) {
|
||||
}
|
||||
|
||||
HalfCycles MFP68901::get_next_sequence_point() {
|
||||
return HalfCycles(-1);
|
||||
return HalfCycles::max();
|
||||
}
|
||||
|
||||
// MARK: - Timers
|
||||
|
||||
@@ -708,9 +708,9 @@ HalfCycles Base::half_cycles_before_internal_cycles(int internal_cycles) {
|
||||
return HalfCycles(((internal_cycles << 2) + (2 - cycles_error_)) / 3);
|
||||
}
|
||||
|
||||
HalfCycles TMS9918::get_time_until_interrupt() {
|
||||
if(!generate_interrupts_ && !enable_line_interrupts_) return HalfCycles(-1);
|
||||
if(get_interrupt_line()) return HalfCycles(0);
|
||||
HalfCycles TMS9918::get_next_sequence_point() {
|
||||
if(!generate_interrupts_ && !enable_line_interrupts_) return HalfCycles::max();
|
||||
if(get_interrupt_line()) return HalfCycles::max();
|
||||
|
||||
// Calculate the amount of time until the next end-of-frame interrupt.
|
||||
const int frame_length = 342 * mode_timing_.total_lines;
|
||||
@@ -750,7 +750,7 @@ HalfCycles TMS9918::get_time_until_interrupt() {
|
||||
if(next_line_interrupt_row == -1) {
|
||||
return generate_interrupts_ ?
|
||||
half_cycles_before_internal_cycles(time_until_frame_interrupt) :
|
||||
HalfCycles(-1);
|
||||
HalfCycles::max();
|
||||
}
|
||||
|
||||
// Figure out the number of internal cycles until the next line interrupt, which is the amount
|
||||
|
||||
@@ -75,13 +75,13 @@ class TMS9918: public Base {
|
||||
void latch_horizontal_counter();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until @c get_interrupt_line would next return true if
|
||||
Returns the amount of time until @c get_interrupt_line would next change if
|
||||
there are no interceding calls to @c write or to @c read.
|
||||
|
||||
If get_interrupt_line is true now, returns zero. If get_interrupt_line would
|
||||
never return true, returns -1.
|
||||
If get_interrupt_line is true now of if get_interrupt_line would
|
||||
never return true, returns HalfCycles::max().
|
||||
*/
|
||||
HalfCycles get_time_until_interrupt();
|
||||
HalfCycles get_next_sequence_point();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until the nominated line interrupt position is
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
#include "../../Concurrency/AsyncTaskQueue.hpp"
|
||||
|
||||
#include "../../Reflection/Struct.hpp"
|
||||
|
||||
namespace GI {
|
||||
namespace AY38910 {
|
||||
|
||||
@@ -162,6 +164,8 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
|
||||
uint8_t a_left_ = 255, a_right_ = 255;
|
||||
uint8_t b_left_ = 255, b_right_ = 255;
|
||||
uint8_t c_left_ = 255, c_right_ = 255;
|
||||
|
||||
friend struct State;
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -169,19 +173,21 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
|
||||
AY-deploying machines of the era.
|
||||
*/
|
||||
struct Utility {
|
||||
template <typename AY> static void select_register(AY &ay, uint8_t reg) {
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
ay.set_data_input(reg);
|
||||
template <typename AY> static void write(AY &ay, bool is_data_write, uint8_t data) {
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | (is_data_write ? 0 : GI::AY38910::BC1)));
|
||||
ay.set_data_input(data);
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
}
|
||||
|
||||
template <typename AY> static void select_register(AY &ay, uint8_t reg) {
|
||||
write(ay, false, reg);
|
||||
}
|
||||
|
||||
template <typename AY> static void write_data(AY &ay, uint8_t reg) {
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2));
|
||||
ay.set_data_input(reg);
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
write(ay, true, reg);
|
||||
}
|
||||
|
||||
template <typename AY> static uint8_t read_data(AY &ay) {
|
||||
template <typename AY> static uint8_t read(AY &ay) {
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
const uint8_t result = ay.get_data_output();
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
@@ -190,6 +196,29 @@ struct Utility {
|
||||
|
||||
};
|
||||
|
||||
struct State: public Reflection::StructImpl<State> {
|
||||
uint8_t registers[16]{};
|
||||
uint8_t selected_register = 0;
|
||||
|
||||
// TODO: all audio-production thread state.
|
||||
|
||||
State() {
|
||||
if(needs_declare()) {
|
||||
DeclareField(registers);
|
||||
DeclareField(selected_register);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename AY> void apply(AY &target) {
|
||||
// Establish emulator-thread state
|
||||
for(uint8_t c = 0; c < 16; c++) {
|
||||
target.select_register(c);
|
||||
target.set_register_value(registers[c]);
|
||||
}
|
||||
target.select_register(selected_register);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
using namespace Concurrency;
|
||||
|
||||
AsyncTaskQueue::AsyncTaskQueue()
|
||||
#ifndef __APPLE__
|
||||
#ifndef USE_GCD
|
||||
: should_destruct_(false)
|
||||
#endif
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
#ifdef USE_GCD
|
||||
serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL);
|
||||
#else
|
||||
thread_ = std::make_unique<std::thread>([this]() {
|
||||
@@ -44,7 +44,7 @@ AsyncTaskQueue::AsyncTaskQueue()
|
||||
}
|
||||
|
||||
AsyncTaskQueue::~AsyncTaskQueue() {
|
||||
#ifdef __APPLE__
|
||||
#ifdef USE_GCD
|
||||
flush();
|
||||
dispatch_release(serial_dispatch_queue_);
|
||||
serial_dispatch_queue_ = nullptr;
|
||||
@@ -57,7 +57,7 @@ AsyncTaskQueue::~AsyncTaskQueue() {
|
||||
}
|
||||
|
||||
void AsyncTaskQueue::enqueue(std::function<void(void)> function) {
|
||||
#ifdef __APPLE__
|
||||
#ifdef USE_GCD
|
||||
dispatch_async(serial_dispatch_queue_, ^{function();});
|
||||
#else
|
||||
std::lock_guard lock(queue_mutex_);
|
||||
@@ -67,7 +67,7 @@ void AsyncTaskQueue::enqueue(std::function<void(void)> function) {
|
||||
}
|
||||
|
||||
void AsyncTaskQueue::flush() {
|
||||
#ifdef __APPLE__
|
||||
#ifdef USE_GCD
|
||||
dispatch_sync(serial_dispatch_queue_, ^{});
|
||||
#else
|
||||
auto flush_mutex = std::make_shared<std::mutex>();
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
|
||||
#include <dispatch/dispatch.h>
|
||||
#define USE_GCD
|
||||
#endif
|
||||
|
||||
namespace Concurrency {
|
||||
@@ -47,7 +48,7 @@ class AsyncTaskQueue {
|
||||
void flush();
|
||||
|
||||
private:
|
||||
#ifdef __APPLE__
|
||||
#ifdef USE_GCD
|
||||
dispatch_queue_t serial_dispatch_queue_;
|
||||
#else
|
||||
std::unique_ptr<std::thread> thread_;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#ifndef Joystick_hpp
|
||||
#define Joystick_hpp
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace Inputs {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "Keyboard.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
using namespace Inputs;
|
||||
|
||||
Keyboard::Keyboard(const std::set<Key> &essential_modifiers) : essential_modifiers_(essential_modifiers) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#ifndef CachingExecutor_hpp
|
||||
#define CachingExecutor_hpp
|
||||
|
||||
#include "Sizes.hpp"
|
||||
#include "../Numeric/Sizes.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#ifndef Disassembler_hpp
|
||||
#define Disassembler_hpp
|
||||
|
||||
#include "Sizes.hpp"
|
||||
#include "../Numeric/Sizes.hpp"
|
||||
|
||||
#include <list>
|
||||
#include <map>
|
||||
|
||||
@@ -787,45 +787,43 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
ay_.ay().set_port_handler(&key_state_);
|
||||
|
||||
// construct the list of necessary ROMs
|
||||
const std::string machine_name = "AmstradCPC";
|
||||
std::vector<ROMMachine::ROM> required_roms = {
|
||||
ROMMachine::ROM(machine_name, "the Amstrad Disk Operating System", "amsdos.rom", 16*1024, 0x1fe22ecd)
|
||||
};
|
||||
std::string model_number;
|
||||
uint32_t crcs[2];
|
||||
bool has_amsdos = false;
|
||||
ROM::Name firmware, basic;
|
||||
|
||||
switch(target.model) {
|
||||
default:
|
||||
model_number = "6128";
|
||||
has_128k_ = true;
|
||||
crcs[0] = 0x0219bb74;
|
||||
crcs[1] = 0xca6af63d;
|
||||
break;
|
||||
case Analyser::Static::AmstradCPC::Target::Model::CPC464:
|
||||
model_number = "464";
|
||||
has_128k_ = false;
|
||||
crcs[0] = 0x815752df;
|
||||
crcs[1] = 0x7d9a3bac;
|
||||
firmware = ROM::Name::CPC464Firmware;
|
||||
basic = ROM::Name::CPC464BASIC;
|
||||
break;
|
||||
case Analyser::Static::AmstradCPC::Target::Model::CPC664:
|
||||
model_number = "664";
|
||||
has_128k_ = false;
|
||||
crcs[0] = 0x3f5a6dc4;
|
||||
crcs[1] = 0x32fee492;
|
||||
firmware = ROM::Name::CPC664Firmware;
|
||||
basic = ROM::Name::CPC664BASIC;
|
||||
has_amsdos = true;
|
||||
break;
|
||||
default:
|
||||
firmware = ROM::Name::CPC6128Firmware;
|
||||
basic = ROM::Name::CPC6128BASIC;
|
||||
has_amsdos = true;
|
||||
break;
|
||||
}
|
||||
required_roms.emplace_back(machine_name, "the CPC " + model_number + " firmware", "os" + model_number + ".rom", 16*1024, crcs[0]);
|
||||
required_roms.emplace_back(machine_name, "the CPC " + model_number + " BASIC ROM", "basic" + model_number + ".rom", 16*1024, crcs[1]);
|
||||
|
||||
// fetch and verify the ROMs
|
||||
const auto roms = rom_fetcher(required_roms);
|
||||
|
||||
for(std::size_t index = 0; index < roms.size(); ++index) {
|
||||
auto &data = roms[index];
|
||||
if(!data) throw ROMMachine::Error::MissingROMs;
|
||||
roms_[index] = std::move(*data);
|
||||
roms_[index].resize(16384);
|
||||
ROM::Request request = ROM::Request(firmware) && ROM::Request(basic);
|
||||
if(has_amsdos) {
|
||||
request = request && ROM::Request(ROM::Name::AMSDOS);
|
||||
}
|
||||
|
||||
// Fetch and verify the ROMs.
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
if(has_amsdos) {
|
||||
roms_[ROMType::AMSDOS] = roms.find(ROM::Name::AMSDOS)->second;
|
||||
}
|
||||
roms_[ROMType::OS] = roms.find(firmware)->second;
|
||||
roms_[ROMType::BASIC] = roms.find(basic)->second;
|
||||
|
||||
// Establish default memory map
|
||||
upper_rom_is_paged_ = true;
|
||||
upper_rom_ = ROMType::BASIC;
|
||||
@@ -1088,6 +1086,7 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
// If there are any tapes supplied, use the first of them.
|
||||
if(!media.tapes.empty()) {
|
||||
tape_player_.set_tape(media.tapes.front());
|
||||
set_use_fast_tape_hack();
|
||||
}
|
||||
|
||||
// Insert up to four disks.
|
||||
|
||||
@@ -151,7 +151,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
#undef SHIFT
|
||||
#undef X
|
||||
|
||||
return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
|
||||
return table_lookup_sequence_for_character(key_sequences, character);
|
||||
}
|
||||
|
||||
bool CharacterMapper::needs_pause_after_key(uint16_t key) const {
|
||||
|
||||
@@ -16,6 +16,7 @@ void Keyboard::perform_command(const Command &command) {
|
||||
switch(command.type) {
|
||||
case Command::Type::Reset:
|
||||
modifiers_ = 0xffff;
|
||||
[[fallthrough]];
|
||||
case Command::Type::Flush: {
|
||||
std::lock_guard lock_guard(keys_mutex_);
|
||||
pending_events_.clear();
|
||||
|
||||
@@ -48,7 +48,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
public MachineTypes::MappedKeyboardMachine,
|
||||
public MachineTypes::JoystickMachine,
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public Inputs::Keyboard,
|
||||
public Configurable::Device,
|
||||
public Activity::Source,
|
||||
public Apple::II::Card::Delegate {
|
||||
@@ -66,7 +65,11 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
uint8_t *ram_, *aux_ram_;
|
||||
};
|
||||
|
||||
CPU::MOS6502::Processor<(model == Analyser::Static::AppleII::Target::Model::EnhancedIIe) ? CPU::MOS6502::Personality::PSynertek65C02 : CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
|
||||
using Processor = CPU::MOS6502::Processor<
|
||||
(model == Analyser::Static::AppleII::Target::Model::EnhancedIIe) ? CPU::MOS6502::Personality::PSynertek65C02 : CPU::MOS6502::Personality::P6502,
|
||||
ConcreteMachine,
|
||||
false>;
|
||||
Processor m6502_;
|
||||
VideoBusHandler video_bus_handler_;
|
||||
Apple::II::Video::Video<VideoBusHandler, is_iie()> video_;
|
||||
int cycles_into_current_line_ = 0;
|
||||
@@ -91,16 +94,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
|
||||
uint8_t ram_[65536], aux_ram_[65536];
|
||||
std::vector<uint8_t> rom_;
|
||||
uint8_t keyboard_input_ = 0x00;
|
||||
bool key_is_down_ = false;
|
||||
|
||||
uint8_t get_keyboard_input() {
|
||||
if(string_serialiser_) {
|
||||
return string_serialiser_->head() | 0x80;
|
||||
} else {
|
||||
return keyboard_input_;
|
||||
}
|
||||
}
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
@@ -258,16 +251,99 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
state.region_20_40.write ? &aux_ram_[0x2000] : &ram_[0x2000]);
|
||||
}
|
||||
|
||||
// MARK - typing
|
||||
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
|
||||
// MARK: - Keyboard and typing.
|
||||
|
||||
// MARK - Joysticks.
|
||||
struct Keyboard: public Inputs::Keyboard {
|
||||
Keyboard(Processor *m6502) : m6502_(m6502) {}
|
||||
|
||||
void reset_all_keys() final {
|
||||
open_apple_is_pressed = closed_apple_is_pressed = key_is_down = false;
|
||||
}
|
||||
|
||||
bool set_key_pressed(Key key, char value, bool is_pressed) final {
|
||||
// If no ASCII value is supplied, look for a few special cases.
|
||||
switch(key) {
|
||||
case Key::Left: value = 0x08; break;
|
||||
case Key::Right: value = 0x15; break;
|
||||
case Key::Down: value = 0x0a; break;
|
||||
case Key::Up: value = 0x0b; break;
|
||||
case Key::Backspace: value = 0x7f; break;
|
||||
case Key::Enter: value = 0x0d; break;
|
||||
case Key::Tab: value = '\t'; break;
|
||||
case Key::Escape: value = 0x1b; break;
|
||||
|
||||
case Key::LeftOption:
|
||||
case Key::RightMeta:
|
||||
open_apple_is_pressed = is_pressed;
|
||||
return true;
|
||||
|
||||
case Key::RightOption:
|
||||
case Key::LeftMeta:
|
||||
closed_apple_is_pressed = is_pressed;
|
||||
return true;
|
||||
|
||||
case Key::F1: case Key::F2: case Key::F3: case Key::F4:
|
||||
case Key::F5: case Key::F6: case Key::F7: case Key::F8:
|
||||
case Key::F9: case Key::F10: case Key::F11: case Key::F12:
|
||||
case Key::PrintScreen:
|
||||
case Key::ScrollLock:
|
||||
case Key::Pause:
|
||||
case Key::Insert:
|
||||
case Key::Home:
|
||||
case Key::PageUp:
|
||||
case Key::PageDown:
|
||||
case Key::End:
|
||||
// Accept a bunch non-symbolic other keys, as
|
||||
// reset, in the hope that the user can find
|
||||
// at least one usable key.
|
||||
m6502_->set_reset_line(is_pressed);
|
||||
return true;
|
||||
|
||||
default:
|
||||
if(!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prior to the IIe, the keyboard could produce uppercase only.
|
||||
if(!is_iie()) value = char(toupper(value));
|
||||
break;
|
||||
}
|
||||
|
||||
if(is_pressed) {
|
||||
keyboard_input = uint8_t(value | 0x80);
|
||||
key_is_down = true;
|
||||
} else {
|
||||
if((keyboard_input & 0x7f) == value) {
|
||||
key_is_down = false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t get_keyboard_input() {
|
||||
if(string_serialiser) {
|
||||
return string_serialiser->head() | 0x80;
|
||||
} else {
|
||||
return keyboard_input;
|
||||
}
|
||||
}
|
||||
|
||||
// The IIe has three keys that are wired directly to the same input as the joystick buttons.
|
||||
bool open_apple_is_pressed = false;
|
||||
bool closed_apple_is_pressed = false;
|
||||
uint8_t keyboard_input = 0x00;
|
||||
bool key_is_down = false;
|
||||
std::unique_ptr<Utility::StringSerialiser> string_serialiser;
|
||||
|
||||
private:
|
||||
Processor *const m6502_;
|
||||
};
|
||||
Keyboard keyboard_;
|
||||
|
||||
// MARK: - Joysticks.
|
||||
JoystickPair joysticks_;
|
||||
|
||||
// The IIe has three keys that are wired directly to the same input as the joystick buttons.
|
||||
bool open_apple_is_pressed_ = false;
|
||||
bool closed_apple_is_pressed_ = false;
|
||||
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::AppleII::Target &target, const ROMMachine::ROMFetcher &rom_fetcher):
|
||||
m6502_(*this),
|
||||
@@ -276,7 +352,8 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
audio_toggle_(audio_queue_),
|
||||
speaker_(audio_toggle_),
|
||||
language_card_(*this),
|
||||
auxiliary_switches_(*this) {
|
||||
auxiliary_switches_(*this),
|
||||
keyboard_(&m6502_) {
|
||||
// The system's master clock rate.
|
||||
constexpr float master_clock = 14318180.0;
|
||||
|
||||
@@ -300,49 +377,55 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
|
||||
// Pick the required ROMs.
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
const std::string machine_name = "AppleII";
|
||||
std::vector<ROMMachine::ROM> rom_descriptions;
|
||||
size_t rom_size = 12*1024;
|
||||
ROM::Name character, system;
|
||||
|
||||
switch(target.model) {
|
||||
default:
|
||||
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::II));
|
||||
rom_descriptions.emplace_back(machine_name, "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588);
|
||||
character = ROM::Name::AppleIICharacter;
|
||||
system = ROM::Name::AppleIIOriginal;
|
||||
break;
|
||||
case Target::Model::IIplus:
|
||||
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::II));
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26);
|
||||
character = ROM::Name::AppleIICharacter;
|
||||
system = ROM::Name::AppleIIPlus;
|
||||
break;
|
||||
case Target::Model::IIe:
|
||||
rom_size += 3840;
|
||||
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::IIe));
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18d);
|
||||
character = ROM::Name::AppleIIeCharacter;
|
||||
system = ROM::Name::AppleIIe;
|
||||
break;
|
||||
case Target::Model::EnhancedIIe:
|
||||
rom_size += 3840;
|
||||
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::EnhancedIIe));
|
||||
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942);
|
||||
character = ROM::Name::AppleIIEnhancedECharacter;
|
||||
system = ROM::Name::AppleIIEnhancedE;
|
||||
break;
|
||||
}
|
||||
const auto roms = rom_fetcher(rom_descriptions);
|
||||
|
||||
// Try to install a Disk II card now, before checking the ROM list,
|
||||
// to make sure that Disk II dependencies have been communicated.
|
||||
if(target.disk_controller != Target::DiskController::None) {
|
||||
ROM::Request request = ROM::Request(character) && ROM::Request(system);
|
||||
|
||||
// Add the necessary Disk II requests if appropriate.
|
||||
const bool has_disk_controller = target.disk_controller != Target::DiskController::None;
|
||||
const bool is_sixteen_sector = target.disk_controller == Target::DiskController::SixteenSector;
|
||||
if(has_disk_controller) {
|
||||
// Apple recommended slot 6 for the (first) Disk II.
|
||||
install_card(6, new Apple::II::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector));
|
||||
request = request && DiskIICard::rom_request(is_sixteen_sector);
|
||||
}
|
||||
|
||||
// Now, check and move the ROMs.
|
||||
if(!roms[0] || !roms[1]) {
|
||||
// Request, validate and install ROMs.
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
rom_ = std::move(*roms[1]);
|
||||
if(rom_.size() > rom_size) {
|
||||
rom_.erase(rom_.begin(), rom_.end() - off_t(rom_size));
|
||||
if(has_disk_controller) {
|
||||
install_card(6, new Apple::II::DiskIICard(roms, is_sixteen_sector));
|
||||
}
|
||||
|
||||
video_.set_character_rom(*roms[0]);
|
||||
rom_ = std::move(roms.find(system)->second);
|
||||
// The IIe and Enhanced IIe ROMs often distributed are oversized; trim if necessary.
|
||||
if(system == ROM::Name::AppleIIe || system == ROM::Name::AppleIIEnhancedE) {
|
||||
if(rom_.size() > 16128) {
|
||||
rom_.erase(rom_.begin(), rom_.end() - off_t(16128));
|
||||
}
|
||||
}
|
||||
video_.set_character_rom(roms.find(character)->second);
|
||||
|
||||
// Set up the default memory blocks. On a II or II+ these values will never change.
|
||||
// On a IIe they'll be affected by selection of auxiliary RAM.
|
||||
@@ -435,18 +518,18 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
default: break;
|
||||
|
||||
case 0xc000:
|
||||
*value = get_keyboard_input();
|
||||
*value = keyboard_.get_keyboard_input();
|
||||
break;
|
||||
case 0xc001: case 0xc002: case 0xc003: case 0xc004: case 0xc005: case 0xc006: case 0xc007:
|
||||
case 0xc008: case 0xc009: case 0xc00a: case 0xc00b: case 0xc00c: case 0xc00d: case 0xc00e: case 0xc00f:
|
||||
*value = (*value & 0x80) | (get_keyboard_input() & 0x7f);
|
||||
*value = (*value & 0x80) | (keyboard_.get_keyboard_input() & 0x7f);
|
||||
break;
|
||||
|
||||
case 0xc061: // Switch input 0.
|
||||
*value &= 0x7f;
|
||||
if(
|
||||
joysticks_.button(0) ||
|
||||
(is_iie() && open_apple_is_pressed_)
|
||||
(is_iie() && keyboard_.open_apple_is_pressed)
|
||||
)
|
||||
*value |= 0x80;
|
||||
break;
|
||||
@@ -454,7 +537,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
*value &= 0x7f;
|
||||
if(
|
||||
joysticks_.button(1) ||
|
||||
(is_iie() && closed_apple_is_pressed_)
|
||||
(is_iie() && keyboard_.closed_apple_is_pressed)
|
||||
)
|
||||
*value |= 0x80;
|
||||
break;
|
||||
@@ -476,7 +559,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
} break;
|
||||
|
||||
// The IIe-only state reads follow...
|
||||
#define IIeSwitchRead(s) *value = get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00);
|
||||
#define IIeSwitchRead(s) *value = keyboard_.get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00);
|
||||
case 0xc011: IIeSwitchRead(language_card_.state().bank2); break;
|
||||
case 0xc012: IIeSwitchRead(language_card_.state().read); break;
|
||||
case 0xc013: IIeSwitchRead(auxiliary_switches_.switches().read_auxiliary_memory); break;
|
||||
@@ -559,15 +642,15 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
break;
|
||||
|
||||
case 0xc010:
|
||||
keyboard_input_ &= 0x7f;
|
||||
if(string_serialiser_) {
|
||||
if(!string_serialiser_->advance())
|
||||
string_serialiser_.reset();
|
||||
keyboard_.keyboard_input &= 0x7f;
|
||||
if(keyboard_.string_serialiser) {
|
||||
if(!keyboard_.string_serialiser->advance())
|
||||
keyboard_.string_serialiser.reset();
|
||||
}
|
||||
|
||||
// On the IIe, reading C010 returns additional key info.
|
||||
if(is_iie() && isReadOperation(operation)) {
|
||||
*value = (key_is_down_ ? 0x80 : 0x00) | (keyboard_input_ & 0x7f);
|
||||
*value = (keyboard_.key_is_down ? 0x80 : 0x00) | (keyboard_.keyboard_input & 0x7f);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -683,81 +766,16 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void reset_all_keys() final {
|
||||
open_apple_is_pressed_ = closed_apple_is_pressed_ = key_is_down_ = false;
|
||||
}
|
||||
|
||||
bool prefers_logical_input() final {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set_key_pressed(Key key, char value, bool is_pressed) final {
|
||||
// If no ASCII value is supplied, look for a few special cases.
|
||||
switch(key) {
|
||||
case Key::Left: value = 0x08; break;
|
||||
case Key::Right: value = 0x15; break;
|
||||
case Key::Down: value = 0x0a; break;
|
||||
case Key::Up: value = 0x0b; break;
|
||||
case Key::Backspace: value = 0x7f; break;
|
||||
case Key::Enter: value = 0x0d; break;
|
||||
case Key::Tab: value = '\t'; break;
|
||||
case Key::Escape: value = 0x1b; break;
|
||||
|
||||
case Key::LeftOption:
|
||||
case Key::RightMeta:
|
||||
open_apple_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
|
||||
case Key::RightOption:
|
||||
case Key::LeftMeta:
|
||||
closed_apple_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
|
||||
case Key::F1: case Key::F2: case Key::F3: case Key::F4:
|
||||
case Key::F5: case Key::F6: case Key::F7: case Key::F8:
|
||||
case Key::F9: case Key::F10: case Key::F11: case Key::F12:
|
||||
case Key::PrintScreen:
|
||||
case Key::ScrollLock:
|
||||
case Key::Pause:
|
||||
case Key::Insert:
|
||||
case Key::Home:
|
||||
case Key::PageUp:
|
||||
case Key::PageDown:
|
||||
case Key::End:
|
||||
// Accept a bunch non-symbolic other keys, as
|
||||
// reset, in the hope that the user can find
|
||||
// at least one usable key.
|
||||
m6502_.set_reset_line(is_pressed);
|
||||
return true;
|
||||
|
||||
default:
|
||||
if(!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prior to the IIe, the keyboard could produce uppercase only.
|
||||
if(!is_iie()) value = char(toupper(value));
|
||||
break;
|
||||
}
|
||||
|
||||
if(is_pressed) {
|
||||
keyboard_input_ = uint8_t(value | 0x80);
|
||||
key_is_down_ = true;
|
||||
} else {
|
||||
if((keyboard_input_ & 0x7f) == value) {
|
||||
key_is_down_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Inputs::Keyboard &get_keyboard() final {
|
||||
return *this;
|
||||
return keyboard_;
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) final {
|
||||
string_serialiser_ = std::make_unique<Utility::StringSerialiser>(string, true);
|
||||
keyboard_.string_serialiser = std::make_unique<Utility::StringSerialiser>(string, true);
|
||||
}
|
||||
|
||||
bool can_type(char c) const final {
|
||||
@@ -769,12 +787,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
std::unique_ptr<Reflection::Struct> get_options() final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->output = get_video_signal_configurable();
|
||||
options->use_square_pixels = video_.get_use_square_pixels();
|
||||
return options;
|
||||
}
|
||||
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
set_video_signal_configurable(options->output);
|
||||
video_.set_use_square_pixels(options->use_square_pixels);
|
||||
}
|
||||
|
||||
// MARK: MediaTarget
|
||||
|
||||
@@ -30,8 +30,11 @@ class Machine {
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType) : Configurable::DisplayOption<Options>(Configurable::Display::CompositeColour) {
|
||||
bool use_square_pixels = false;
|
||||
|
||||
Options(Configurable::OptionsType) : Configurable::DisplayOption<Options>(Configurable::Display::CompositeColour) {
|
||||
if(needs_declare()) {
|
||||
DeclareField(use_square_pixels);
|
||||
declare_display_option();
|
||||
limit_enum(&output, Configurable::Display::CompositeMonochrome, Configurable::Display::CompositeColour, -1);
|
||||
}
|
||||
|
||||
@@ -10,27 +10,34 @@
|
||||
|
||||
using namespace Apple::II;
|
||||
|
||||
DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector) : diskii_(2045454) {
|
||||
std::vector<std::unique_ptr<std::vector<uint8_t>>> roms;
|
||||
ROM::Request DiskIICard::rom_request(bool is_16_sector) {
|
||||
if(is_16_sector) {
|
||||
roms = rom_fetcher({
|
||||
{"DiskII", "the Disk II 16-sector boot ROM", "boot-16.rom", 256, 0xce7144f6},
|
||||
{"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 } }
|
||||
});
|
||||
return ROM::Request(ROM::Name::DiskIIBoot16Sector) && ROM::Request(ROM::Name::DiskIIStateMachine16Sector);
|
||||
} else {
|
||||
roms = rom_fetcher({
|
||||
{"DiskII", "the Disk II 13-sector boot ROM", "boot-13.rom", 256, 0xd34eb2ff},
|
||||
{"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 } }
|
||||
// {"DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620 }
|
||||
/* TODO: once the DiskII knows how to decode common images of the 13-sector state machine, use that instead of the 16-sector. */
|
||||
});
|
||||
/* TODO: once the DiskII knows how to decode common images of the 13-sector state machine, use that instead of the 16-sector. */
|
||||
return ROM::Request(ROM::Name::DiskIIBoot13Sector) && ROM::Request(ROM::Name::DiskIIStateMachine16Sector);
|
||||
}
|
||||
if(!roms[0] || !roms[1]) {
|
||||
}
|
||||
|
||||
|
||||
DiskIICard::DiskIICard(ROM::Map &map, bool is_16_sector) : diskii_(2045454) {
|
||||
std::vector<std::unique_ptr<std::vector<uint8_t>>> roms;
|
||||
ROM::Map::iterator state_machine, boot;
|
||||
if(is_16_sector) {
|
||||
state_machine = map.find(ROM::Name::DiskIIStateMachine16Sector);
|
||||
boot = map.find(ROM::Name::DiskIIBoot16Sector);
|
||||
} else {
|
||||
// TODO: see above re: 13-sector state machine.
|
||||
state_machine = map.find(ROM::Name::DiskIIStateMachine16Sector);
|
||||
boot = map.find(ROM::Name::DiskIIBoot13Sector);
|
||||
}
|
||||
|
||||
if(state_machine == map.end() || boot == map.end()) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
boot_ = std::move(*roms[0]);
|
||||
diskii_.set_state_machine(*roms[1]);
|
||||
boot_ = std::move(boot->second);
|
||||
diskii_.set_state_machine(state_machine->second);
|
||||
set_select_constraints(None);
|
||||
diskii_.set_clocking_hint_observer(this);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ namespace II {
|
||||
|
||||
class DiskIICard: public Card, public ClockingHint::Observer {
|
||||
public:
|
||||
DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector);
|
||||
static ROM::Request rom_request(bool is_16_sector);
|
||||
DiskIICard(ROM::Map &, bool is_16_sector);
|
||||
|
||||
void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final;
|
||||
void run_for(Cycles cycles, int stretches) final;
|
||||
|
||||
@@ -15,9 +15,8 @@ VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
|
||||
crt_(910, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance1),
|
||||
is_iie_(is_iie) {
|
||||
|
||||
// Show only the centre 75% of the TV frame.
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::CompositeColour);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.118f, 0.122f, 0.77f, 0.77f));
|
||||
set_use_square_pixels(use_square_pixels_);
|
||||
|
||||
// TODO: there seems to be some sort of bug whereby switching modes can cause
|
||||
// a signal discontinuity that knocks phase out of whack. So it isn't safe to
|
||||
@@ -26,6 +25,41 @@ VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
|
||||
// crt_.set_immediate_default_phase(0.5f);
|
||||
}
|
||||
|
||||
void VideoBase::set_use_square_pixels(bool use_square_pixels) {
|
||||
use_square_pixels_ = use_square_pixels;
|
||||
|
||||
// HYPER-UGLY HACK. See correlated hack in the Macintosh.
|
||||
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.128f, 0.122f, 0.75f, 0.77f));
|
||||
#else
|
||||
if(use_square_pixels) {
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.128f, 0.09f, 0.75f, 0.77f));
|
||||
} else {
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.128f, 0.12f, 0.75f, 0.77f));
|
||||
}
|
||||
#endif
|
||||
|
||||
if(use_square_pixels) {
|
||||
// From what I can make out, many contemporary Apple II monitors were
|
||||
// calibrated slightly to stretch the Apple II's display slightly wider
|
||||
// than it should be per the NTSC standards, for approximately square
|
||||
// pixels. This reproduces that.
|
||||
|
||||
// 243 lines and 52µs are visible.
|
||||
// i.e. to be square, 1 pixel should be: (1/243 * 52) * (3/4) = 156/972 = 39/243 µs
|
||||
// On an Apple II each pixel is actually 1/7µs.
|
||||
// Therefore the adjusted aspect ratio should be (4/3) * (39/243)/(1/7) = (4/3) * 273/243 = 1092/729 = 343/243 ~= 1.412
|
||||
crt_.set_aspect_ratio(343.0f / 243.0f);
|
||||
} else {
|
||||
// Standard NTSC aspect ratio.
|
||||
crt_.set_aspect_ratio(4.0f / 3.0f);
|
||||
}
|
||||
}
|
||||
bool VideoBase::get_use_square_pixels() {
|
||||
return use_square_pixels_;
|
||||
}
|
||||
|
||||
|
||||
void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
@@ -51,8 +51,14 @@ class VideoBase: public VideoSwitches<Cycles> {
|
||||
/// Gets the type of output.
|
||||
Outputs::Display::DisplayType get_display_type() const;
|
||||
|
||||
/// Sets whether the current CRT should be recalibrated away from normative NTSC
|
||||
/// to produce square pixels in 40-column text mode.
|
||||
void set_use_square_pixels(bool);
|
||||
bool get_use_square_pixels();
|
||||
|
||||
protected:
|
||||
Outputs::CRT::CRT crt_;
|
||||
bool use_square_pixels_ = false;
|
||||
|
||||
// State affecting output video stream generation.
|
||||
uint8_t *pixel_pointer_ = nullptr;
|
||||
@@ -393,7 +399,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
// The OpenGL scan target introduces a phase error of 1/8th of a wave. The Metal one does not.
|
||||
// Supply the real phase value if this is an Apple build.
|
||||
// TODO: eliminate UGLY HACK.
|
||||
#ifdef __APPLE__
|
||||
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
|
||||
constexpr int phase = 224;
|
||||
#else
|
||||
constexpr int phase = 0;
|
||||
|
||||
@@ -228,36 +228,6 @@ template <typename TimeUnit> class VideoSwitches {
|
||||
return external_.annunciator_3;
|
||||
}
|
||||
|
||||
enum class CharacterROM {
|
||||
/// The ROM that shipped with both the Apple II and the II+.
|
||||
II,
|
||||
/// The ROM that shipped with the original IIe.
|
||||
IIe,
|
||||
/// The ROM that shipped with the Enhanced IIe.
|
||||
EnhancedIIe,
|
||||
/// The ROM that shipped with the IIgs.
|
||||
IIgs
|
||||
};
|
||||
|
||||
/// @returns A file-level description of @c rom.
|
||||
static ROMMachine::ROM rom_description(CharacterROM rom) {
|
||||
const std::string machine_name = "AppleII";
|
||||
switch(rom) {
|
||||
case CharacterROM::II:
|
||||
return ROMMachine::ROM(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
|
||||
|
||||
case CharacterROM::IIe:
|
||||
return ROMMachine::ROM(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1);
|
||||
|
||||
default: // To appease GCC.
|
||||
case CharacterROM::EnhancedIIe:
|
||||
return ROMMachine::ROM(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d);
|
||||
|
||||
case CharacterROM::IIgs:
|
||||
return ROMMachine::ROM(machine_name, "the Apple IIgs character ROM", "apple2gs.chr", 4*1024, 0x91e53cd8);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the character ROM for this video output.
|
||||
void set_character_rom(const std::vector<uint8_t> &rom) {
|
||||
character_rom_ = rom;
|
||||
|
||||
@@ -189,31 +189,23 @@ class ConcreteMachine:
|
||||
speaker_.set_input_rate(float(CLOCK_RATE) / float(audio_divider));
|
||||
|
||||
using Target = Analyser::Static::AppleIIgs::Target;
|
||||
std::vector<ROMMachine::ROM> rom_descriptions;
|
||||
const std::string machine_name = "AppleIIgs";
|
||||
ROM::Name system;
|
||||
switch(target.model) {
|
||||
case Target::Model::ROM00:
|
||||
/* TODO */
|
||||
case Target::Model::ROM01:
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIgs ROM01", "apple2gs.rom", 128*1024, 0x42f124b0);
|
||||
break;
|
||||
|
||||
case Target::Model::ROM03:
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIgs ROM03", "apple2gs.rom2", 256*1024, 0xde7ddf29);
|
||||
break;
|
||||
case Target::Model::ROM00: system = ROM::Name::AppleIIgsROM00; break;
|
||||
case Target::Model::ROM01: system = ROM::Name::AppleIIgsROM01; break;
|
||||
default: system = ROM::Name::AppleIIgsROM03; break;
|
||||
}
|
||||
rom_descriptions.push_back(video_->rom_description(Video::Video::CharacterROM::EnhancedIIe));
|
||||
constexpr ROM::Name characters = ROM::Name::AppleIIEnhancedECharacter;
|
||||
constexpr ROM::Name microcontroller = ROM::Name::AppleIIgsMicrocontrollerROM03;
|
||||
|
||||
// TODO: pick a different ADB ROM for earlier machine revisions?
|
||||
rom_descriptions.emplace_back(machine_name, "the Apple IIgs ADB microcontroller ROM", "341s0632-2", 4*1024, 0xe1c11fb0);
|
||||
|
||||
const auto roms = rom_fetcher(rom_descriptions);
|
||||
if(!roms[0] || !roms[1] || !roms[2]) {
|
||||
ROM::Request request = ROM::Request(system) && ROM::Request(characters) && ROM::Request(microcontroller);
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
rom_ = *roms[0];
|
||||
video_->set_character_rom(*roms[1]);
|
||||
adb_glu_->set_microcontroller_rom(*roms[2]);
|
||||
rom_ = roms.find(system)->second;
|
||||
video_->set_character_rom(roms.find(characters)->second);
|
||||
adb_glu_->set_microcontroller_rom(roms.find(microcontroller)->second);
|
||||
|
||||
// Run only the currently-interesting self test.
|
||||
// rom_[0x36402] = 2;
|
||||
@@ -1093,10 +1085,10 @@ class ConcreteMachine:
|
||||
// MARK: - Other components.
|
||||
|
||||
Apple::Clock::ParallelClock clock_;
|
||||
JustInTimeActor<Apple::IIgs::Video::Video, 1, 2, Cycles> video_; // i.e. run video at 7Mhz.
|
||||
JustInTimeActor<Apple::IIgs::ADB::GLU, 1, 4, Cycles> adb_glu_; // i.e. 3,579,545Mhz.
|
||||
JustInTimeActor<Apple::IIgs::Video::Video, Cycles, 1, 2> video_; // i.e. run video at 7Mhz.
|
||||
JustInTimeActor<Apple::IIgs::ADB::GLU, Cycles, 1, 4> adb_glu_; // i.e. 3,579,545Mhz.
|
||||
Zilog::SCC::z8530 scc_;
|
||||
JustInTimeActor<Apple::IWM, 1, 2, Cycles> iwm_;
|
||||
JustInTimeActor<Apple::IWM, Cycles, 1, 2> iwm_;
|
||||
Cycles cycles_since_clock_tick_;
|
||||
Apple::Macintosh::DoubleDensityDrive drives35_[2];
|
||||
Apple::Disk::DiskIIDrive drives525_[2];
|
||||
|
||||
@@ -576,7 +576,7 @@ class MemoryMap {
|
||||
if(region.write) { \
|
||||
region.write[address] = *value; \
|
||||
const bool _mm_is_shadowed = IsShadowed(map, region, address); \
|
||||
map.shadow_base[is_shadowed][(®ion.write[address] - map.ram_base) & map.shadow_mask[_mm_is_shadowed]] = *value; \
|
||||
map.shadow_base[_mm_is_shadowed][(®ion.write[address] - map.ram_base) & map.shadow_mask[_mm_is_shadowed]] = *value; \
|
||||
}
|
||||
|
||||
// Quick notes on ::IsShadowed contortions:
|
||||
|
||||
@@ -271,7 +271,7 @@ class Keyboard {
|
||||
/// so this may not be valid prior to Mode::PerformingCommand.
|
||||
int command_ = 0;
|
||||
/// Populated during PerformingCommand as the response to the most-recently-received command, this
|
||||
/// is then shifted out to teh host computer. So it is guaranteed valid at the beginning of Mode::SendingResponse,
|
||||
/// is then shifted out to the host computer. So it is guaranteed valid at the beginning of Mode::SendingResponse,
|
||||
/// but not afterwards.
|
||||
int response_ = 0;
|
||||
|
||||
|
||||
@@ -96,25 +96,24 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
using Model = Analyser::Static::Macintosh::Target::Model;
|
||||
const std::string machine_name = "Macintosh";
|
||||
uint32_t ram_size, rom_size;
|
||||
std::vector<ROMMachine::ROM> rom_descriptions;
|
||||
ROM::Name rom_name;
|
||||
switch(model) {
|
||||
default:
|
||||
case Model::Mac128k:
|
||||
ram_size = 128*1024;
|
||||
rom_size = 64*1024;
|
||||
rom_descriptions.emplace_back(machine_name, "the Macintosh 128k ROM", "mac128k.rom", 64*1024, 0x6d0c8a28);
|
||||
rom_name = ROM::Name::Macintosh128k;
|
||||
break;
|
||||
case Model::Mac512k:
|
||||
ram_size = 512*1024;
|
||||
rom_size = 64*1024;
|
||||
rom_descriptions.emplace_back(machine_name, "the Macintosh 512k ROM", "mac512k.rom", 64*1024, 0xcf759e0d);
|
||||
rom_name = ROM::Name::Macintosh512k;
|
||||
break;
|
||||
case Model::Mac512ke:
|
||||
case Model::MacPlus: {
|
||||
ram_size = ((model == Model::MacPlus) ? 4096 : 512)*1024;
|
||||
rom_size = 128*1024;
|
||||
const std::initializer_list<uint32_t> crc32s = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e };
|
||||
rom_descriptions.emplace_back(machine_name, "the Macintosh Plus ROM", "macplus.rom", 128*1024, crc32s);
|
||||
rom_name = ROM::Name::MacintoshPlus;
|
||||
} break;
|
||||
}
|
||||
ram_mask_ = ram_size - 1;
|
||||
@@ -123,12 +122,12 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
video_.set_ram(reinterpret_cast<uint16_t *>(ram_.data()), ram_mask_ >> 1);
|
||||
|
||||
// Grab a copy of the ROM and convert it into big-endian data.
|
||||
const auto roms = rom_fetcher(rom_descriptions);
|
||||
if(!roms[0]) {
|
||||
ROM::Request request(rom_name);
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
roms[0]->resize(rom_size);
|
||||
Memory::PackBigEndian16(*roms[0], rom_);
|
||||
Memory::PackBigEndian16(roms.find(rom_name)->second, rom_);
|
||||
|
||||
// Randomise memory contents.
|
||||
Memory::Fuzz(ram_);
|
||||
@@ -647,7 +646,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
return mouse_;
|
||||
}
|
||||
|
||||
using IWMActor = JustInTimeActor<IWM, 1, 1, HalfCycles, Cycles>;
|
||||
using IWMActor = JustInTimeActor<IWM>;
|
||||
|
||||
class VIAPortHandler: public MOS::MOS6522::PortHandler {
|
||||
public:
|
||||
|
||||
@@ -35,7 +35,7 @@ Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulato
|
||||
// The [newer] Metal scan target has no such issue. So assume that Apple => Metal,
|
||||
// and set a visible area to work around the OpenGL issue if required.
|
||||
// TODO: eliminate UGLY HACK.
|
||||
#ifdef __APPLE__
|
||||
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.08f, 10.0f / 368.0f, 0.82f, 344.0f / 368.0f));
|
||||
#else
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));
|
||||
|
||||
@@ -74,15 +74,13 @@ class ConcreteMachine:
|
||||
video_->set_ram(reinterpret_cast<uint16_t *>(ram_.data()), ram_.size());
|
||||
Memory::Fuzz(ram_);
|
||||
|
||||
std::vector<ROMMachine::ROM> rom_descriptions = {
|
||||
{"AtariST", "the UK TOS 1.00 ROM", "tos100.img", 192*1024, 0x1a586c64}
|
||||
// {"AtariST", "the UK TOS 1.04 ROM", "tos104.img", 192*1024, 0xa50d1d43}
|
||||
};
|
||||
const auto roms = rom_fetcher(rom_descriptions);
|
||||
if(!roms[0]) {
|
||||
constexpr ROM::Name rom_name = ROM::Name::AtariSTTOS100;
|
||||
ROM::Request request(rom_name);
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
Memory::PackBigEndian16(*roms[0], rom_);
|
||||
Memory::PackBigEndian16(roms.find(rom_name)->second, rom_);
|
||||
|
||||
// Set up basic memory map.
|
||||
memory_map_[0] = BusDevice::MostlyRAM;
|
||||
@@ -347,18 +345,11 @@ class ConcreteMachine:
|
||||
update_audio();
|
||||
|
||||
if(cycle.operation & Microcycle::Read) {
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
cycle.set_value8_high(ay_.get_data_output());
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
cycle.set_value8_high(GI::AY38910::Utility::read(ay_));
|
||||
} else {
|
||||
// Net effect here: addresses with bit 1 set write to a register,
|
||||
// addresses with bit 1 clear select a register.
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(
|
||||
GI::AY38910::BC2 | GI::AY38910::BDIR
|
||||
| ((address&2) ? 0 : GI::AY38910::BC1)
|
||||
));
|
||||
ay_.set_data_input(cycle.value8_high());
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
GI::AY38910::Utility::write(ay_, address&2, cycle.value8_high());
|
||||
}
|
||||
return delay + HalfCycles(2);
|
||||
|
||||
@@ -486,9 +477,9 @@ class ConcreteMachine:
|
||||
JustInTimeActor<Video> video_;
|
||||
|
||||
// The MFP runs at 819200/2673749ths of the CPU clock rate.
|
||||
JustInTimeActor<Motorola::MFP68901::MFP68901, 819200, 2673749> mfp_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, 16> keyboard_acia_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, 16> midi_acia_;
|
||||
JustInTimeActor<Motorola::MFP68901::MFP68901, HalfCycles, 819200, 2673749> mfp_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, HalfCycles, 16> keyboard_acia_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, HalfCycles, 16> midi_acia_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
|
||||
@@ -252,7 +252,7 @@ void DMAController::set_component_prefers_clocking(ClockingHint::Source *, Clock
|
||||
}
|
||||
|
||||
ClockingHint::Preference DMAController::preferred_clocking() const {
|
||||
return (fdc_.preferred_clocking() == ClockingHint::Preference::None) ? ClockingHint::Preference::None : ClockingHint::Preference::RealTime;
|
||||
return (fdc_.preferred_clocking() == ClockingHint::Preference::None) ? ClockingHint::Preference::JustInTime : ClockingHint::Preference::RealTime;
|
||||
}
|
||||
|
||||
void DMAController::set_activity_observer(Activity::Observer *observer) {
|
||||
|
||||
@@ -127,15 +127,13 @@ class ConcreteMachine:
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
|
||||
const auto roms = rom_fetcher(
|
||||
{ {"ColecoVision", "the ColecoVision BIOS", "coleco.rom", 8*1024, 0x3aa93ef3} });
|
||||
|
||||
if(!roms[0]) {
|
||||
constexpr ROM::Name rom_name = ROM::Name::ColecoVisionBIOS;
|
||||
const ROM::Request request(rom_name);
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
bios_ = *roms[0];
|
||||
bios_.resize(8192);
|
||||
bios_ = roms.find(rom_name)->second;
|
||||
|
||||
if(!target.media.cartridges.empty()) {
|
||||
const auto &segment = target.media.cartridges.front()->get_segments().front();
|
||||
@@ -215,7 +213,9 @@ class ConcreteMachine:
|
||||
}
|
||||
const HalfCycles length = cycle.length + penalty;
|
||||
|
||||
vdp_ += length;
|
||||
if(vdp_ += length) {
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
}
|
||||
time_since_sn76489_update_ += length;
|
||||
|
||||
// Act only if necessary.
|
||||
@@ -263,7 +263,6 @@ class ConcreteMachine:
|
||||
case 5:
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 7: {
|
||||
@@ -287,9 +286,7 @@ class ConcreteMachine:
|
||||
case 0x52:
|
||||
// Read AY data.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
*cycle.value = ay_.get_data_output();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
*cycle.value = GI::AY38910::Utility::read(ay_);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@@ -306,7 +303,6 @@ class ConcreteMachine:
|
||||
case 5:
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 7:
|
||||
@@ -324,16 +320,12 @@ class ConcreteMachine:
|
||||
case 0x50:
|
||||
// Set AY address.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::BC1);
|
||||
ay_.set_data_input(*cycle.value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
GI::AY38910::Utility::select_register(ay_, *cycle.value);
|
||||
break;
|
||||
case 0x51:
|
||||
// Set AY data.
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR));
|
||||
ay_.set_data_input(*cycle.value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
GI::AY38910::Utility::write_data(ay_, *cycle.value);
|
||||
break;
|
||||
case 0x53:
|
||||
super_game_module_.replace_ram = !!((*cycle.value)&0x1);
|
||||
@@ -347,13 +339,6 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
if(time_until_interrupt_ > 0) {
|
||||
time_until_interrupt_ -= length;
|
||||
if(time_until_interrupt_ <= HalfCycles(0)) {
|
||||
z80_.set_non_maskable_interrupt_line(true, time_until_interrupt_);
|
||||
}
|
||||
}
|
||||
|
||||
return penalty;
|
||||
}
|
||||
|
||||
@@ -390,7 +375,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||
JustInTimeActor<TI::TMS::TMS9918, 1, 1, HalfCycles> vdp_;
|
||||
JustInTimeActor<TI::TMS::TMS9918> vdp_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
@@ -414,7 +399,6 @@ class ConcreteMachine:
|
||||
bool joysticks_in_keypad_mode_ = false;
|
||||
|
||||
HalfCycles time_since_sn76489_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
|
||||
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
|
||||
int pc_zero_accesses_ = 0;
|
||||
|
||||
@@ -41,7 +41,8 @@ namespace C1540 {
|
||||
*/
|
||||
class Machine final: public MachineBase {
|
||||
public:
|
||||
Machine(Personality personality, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
static ROM::Request rom_request(Personality personality);
|
||||
Machine(Personality personality, const ROM::Map &roms);
|
||||
|
||||
/*!
|
||||
Sets the serial bus to which this drive should attach itself.
|
||||
|
||||
@@ -16,7 +16,15 @@
|
||||
|
||||
using namespace Commodore::C1540;
|
||||
|
||||
MachineBase::MachineBase(Personality personality, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
ROM::Request Machine::rom_request(Personality personality) {
|
||||
switch(personality) {
|
||||
default:
|
||||
case Personality::C1540: return ROM::Request(ROM::Name::Commodore1540);
|
||||
case Personality::C1541: return ROM::Request(ROM::Name::Commodore1541);
|
||||
}
|
||||
}
|
||||
|
||||
MachineBase::MachineBase(Personality personality, const ROM::Map &roms) :
|
||||
Storage::Disk::Controller(1000000),
|
||||
m6502_(*this),
|
||||
serial_port_VIA_port_handler_(new SerialPortVIA(serial_port_VIA_)),
|
||||
@@ -39,28 +47,22 @@ MachineBase::MachineBase(Personality personality, const ROMMachine::ROMFetcher &
|
||||
emplace_drive(1000000, 300, 2);
|
||||
set_drive(1);
|
||||
|
||||
std::string device_name;
|
||||
uint32_t crc = 0;
|
||||
ROM::Name rom_name;
|
||||
switch(personality) {
|
||||
case Personality::C1540:
|
||||
device_name = "1540";
|
||||
crc = 0x718d42b1;
|
||||
break;
|
||||
case Personality::C1541:
|
||||
device_name = "1541";
|
||||
crc = 0xfb760019;
|
||||
break;
|
||||
default:
|
||||
case Personality::C1540: rom_name = ROM::Name::Commodore1540; break;
|
||||
case Personality::C1541: rom_name = ROM::Name::Commodore1541; break;
|
||||
}
|
||||
|
||||
auto roms = rom_fetcher({ {"Commodore1540", "the " + device_name + " ROM", device_name + ".bin", 16*1024, crc} });
|
||||
if(!roms[0]) {
|
||||
auto rom = roms.find(rom_name);
|
||||
if(rom == roms.end()) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
std::memcpy(rom_, roms[0]->data(), std::min(sizeof(rom_), roms[0]->size()));
|
||||
std::memcpy(rom_, roms.find(rom_name)->second.data(), std::min(sizeof(rom_), roms.find(rom_name)->second.size()));
|
||||
}
|
||||
|
||||
Machine::Machine(Personality personality, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
MachineBase(personality, rom_fetcher) {}
|
||||
Machine::Machine(Personality personality, const ROM::Map &roms) :
|
||||
MachineBase(personality, roms) {}
|
||||
|
||||
void Machine::set_serial_bus(std::shared_ptr<::Commodore::Serial::Bus> serial_bus) {
|
||||
Commodore::Serial::AttachPortAndBus(serial_port_, serial_bus);
|
||||
|
||||
@@ -127,7 +127,7 @@ class MachineBase:
|
||||
public Storage::Disk::Controller {
|
||||
|
||||
public:
|
||||
MachineBase(Personality personality, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
MachineBase(Personality personality, const ROM::Map &roms);
|
||||
|
||||
// to satisfy CPU::MOS6502::Processor
|
||||
Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value);
|
||||
|
||||
@@ -151,5 +151,5 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
#undef SHIFT
|
||||
#undef X
|
||||
|
||||
return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
|
||||
return table_lookup_sequence_for_character(key_sequences, character);
|
||||
}
|
||||
|
||||
@@ -316,53 +316,48 @@ class ConcreteMachine:
|
||||
// Install a joystick.
|
||||
joysticks_.emplace_back(new Joystick(*user_port_via_port_handler_, *keyboard_via_port_handler_));
|
||||
|
||||
const std::string machine_name = "Vic20";
|
||||
std::vector<ROMMachine::ROM> rom_names = {
|
||||
{machine_name, "the VIC-20 BASIC ROM", "basic.bin", 8*1024, 0xdb4c43c1}
|
||||
};
|
||||
ROM::Request request(ROM::Name::Vic20BASIC);
|
||||
ROM::Name kernel, character;
|
||||
switch(target.region) {
|
||||
default:
|
||||
rom_names.emplace_back(machine_name, "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6);
|
||||
rom_names.emplace_back(machine_name, "the English-language PAL VIC-20 kernel ROM", "kernel-pal.bin", 8*1024, 0x4be07cb4);
|
||||
character = ROM::Name::Vic20EnglishCharacters;
|
||||
kernel = ROM::Name::Vic20EnglishPALKernel;
|
||||
break;
|
||||
case Analyser::Static::Commodore::Target::Region::American:
|
||||
rom_names.emplace_back(machine_name, "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6);
|
||||
rom_names.emplace_back(machine_name, "the English-language NTSC VIC-20 kernel ROM", "kernel-ntsc.bin", 8*1024, 0xe5e7c174);
|
||||
character = ROM::Name::Vic20EnglishCharacters;
|
||||
kernel = ROM::Name::Vic20EnglishNTSCKernel;
|
||||
break;
|
||||
case Analyser::Static::Commodore::Target::Region::Danish:
|
||||
rom_names.emplace_back(machine_name, "the Danish VIC-20 character ROM", "characters-danish.bin", 4*1024, 0x7fc11454);
|
||||
rom_names.emplace_back(machine_name, "the Danish VIC-20 kernel ROM", "kernel-danish.bin", 8*1024, 0x02adaf16);
|
||||
character = ROM::Name::Vic20DanishCharacters;
|
||||
kernel = ROM::Name::Vic20DanishKernel;
|
||||
break;
|
||||
case Analyser::Static::Commodore::Target::Region::Japanese:
|
||||
rom_names.emplace_back(machine_name, "the Japanese VIC-20 character ROM", "characters-japanese.bin", 4*1024, 0xfcfd8a4b);
|
||||
rom_names.emplace_back(machine_name, "the Japanese VIC-20 kernel ROM", "kernel-japanese.bin", 8*1024, 0x336900d7);
|
||||
character = ROM::Name::Vic20JapaneseCharacters;
|
||||
kernel = ROM::Name::Vic20JapaneseKernel;
|
||||
break;
|
||||
case Analyser::Static::Commodore::Target::Region::Swedish:
|
||||
rom_names.emplace_back(machine_name, "the Swedish VIC-20 character ROM", "characters-swedish.bin", 4*1024, 0xd808551d);
|
||||
rom_names.emplace_back(machine_name, "the Swedish VIC-20 kernel ROM", "kernel-swedish.bin", 8*1024, 0xb2a60662);
|
||||
character = ROM::Name::Vic20SwedishCharacters;
|
||||
kernel = ROM::Name::Vic20SwedishKernel;
|
||||
break;
|
||||
}
|
||||
|
||||
const auto roms = rom_fetcher(rom_names);
|
||||
if(target.has_c1540) {
|
||||
request = request && Commodore::C1540::Machine::rom_request(Commodore::C1540::Personality::C1540);
|
||||
}
|
||||
request = request && ROM::Request(character) && ROM::Request(kernel);
|
||||
|
||||
for(const auto &rom: roms) {
|
||||
if(!rom) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
basic_rom_ = std::move(*roms[0]);
|
||||
character_rom_ = std::move(*roms[1]);
|
||||
kernel_rom_ = std::move(*roms[2]);
|
||||
|
||||
// Characters ROMs should be 4kb.
|
||||
character_rom_.resize(4096);
|
||||
// Kernel ROMs and the BASIC ROM should be 8kb.
|
||||
kernel_rom_.resize(8192);
|
||||
basic_rom_ = std::move(roms.find(ROM::Name::Vic20BASIC)->second);
|
||||
character_rom_ = std::move(roms.find(character)->second);
|
||||
kernel_rom_ = std::move(roms.find(kernel)->second);
|
||||
|
||||
if(target.has_c1540) {
|
||||
// construct the 1540
|
||||
c1540_ = std::make_unique<::Commodore::C1540::Machine>(Commodore::C1540::Personality::C1540, rom_fetcher);
|
||||
c1540_ = std::make_unique<::Commodore::C1540::Machine>(Commodore::C1540::Personality::C1540, roms);
|
||||
|
||||
// attach it to the serial bus
|
||||
c1540_->set_serial_bus(serial_bus_);
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
#include "../Utility/Typer.hpp"
|
||||
#include "../../Analyser/Static/Acorn/Target.hpp"
|
||||
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include "Interrupts.hpp"
|
||||
#include "Keyboard.hpp"
|
||||
#include "Plus3.hpp"
|
||||
@@ -54,7 +56,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
scsi_bus_(4'000'000),
|
||||
hard_drive_(scsi_bus_, 0),
|
||||
scsi_device_(scsi_bus_.add_device()),
|
||||
video_output_(ram_),
|
||||
video_(ram_),
|
||||
sound_generator_(audio_queue_),
|
||||
speaker_(sound_generator_) {
|
||||
memset(key_states_, 0, sizeof(key_states_));
|
||||
@@ -67,37 +69,25 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
speaker_.set_input_rate(2000000 / SoundGenerator::clock_rate_divider);
|
||||
speaker_.set_high_frequency_cutoff(6000);
|
||||
|
||||
const std::string machine_name = "Electron";
|
||||
std::vector<ROMMachine::ROM> required_roms = {
|
||||
{machine_name, "the Acorn BASIC II ROM", "basic.rom", 16*1024, 0x79434781},
|
||||
{machine_name, "the Electron MOS ROM", "os.rom", 16*1024, 0xbf63fb1f}
|
||||
};
|
||||
const size_t pres_adfs_rom_position = required_roms.size();
|
||||
::ROM::Request request = ::ROM::Request(::ROM::Name::AcornBASICII) && ::ROM::Request(::ROM::Name::AcornElectronMOS100);
|
||||
if(target.has_pres_adfs) {
|
||||
required_roms.emplace_back(machine_name, "the E00 ADFS ROM, first slot", "ADFS-E00_1.rom", 16*1024, 0x51523993);
|
||||
required_roms.emplace_back(machine_name, "the E00 ADFS ROM, second slot", "ADFS-E00_2.rom", 16*1024, 0x8d17de0e);
|
||||
request = request && ::ROM::Request(::ROM::Name::PRESADFSSlot1) && ::ROM::Request(::ROM::Name::PRESADFSSlot2);
|
||||
}
|
||||
const size_t acorn_adfs_rom_position = required_roms.size();
|
||||
if(target.has_acorn_adfs) {
|
||||
required_roms.emplace_back(machine_name, "the Acorn ADFS ROM", "adfs.rom", 16*1024, 0x3289bdc6);
|
||||
request = request && ::ROM::Request(::ROM::Name::AcornADFS);
|
||||
}
|
||||
const size_t dfs_rom_position = required_roms.size();
|
||||
if(target.has_dfs) {
|
||||
required_roms.emplace_back(machine_name, "the 1770 DFS ROM", "DFS-1770-2.20.rom", 16*1024, 0xf3dc9bc5);
|
||||
request = request && ::ROM::Request(::ROM::Name::Acorn1770DFS);
|
||||
}
|
||||
const size_t ap6_rom_position = required_roms.size();
|
||||
if(target.has_ap6_rom) {
|
||||
required_roms.emplace_back(machine_name, "the 8kb Advanced Plus 6 ROM", "AP6v133.rom", 8*1024, 0xe0013cfc);
|
||||
request = request && ::ROM::Request(::ROM::Name::PRESAdvancedPlus6);
|
||||
}
|
||||
const auto roms = rom_fetcher(required_roms);
|
||||
|
||||
for(const auto &rom: roms) {
|
||||
if(!rom) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
set_rom(ROM::BASIC, *roms[0], false);
|
||||
set_rom(ROM::OS, *roms[1], false);
|
||||
set_rom(ROM::BASIC, roms.find(::ROM::Name::AcornBASICII)->second, false);
|
||||
set_rom(ROM::OS, roms.find(::ROM::Name::AcornElectronMOS100)->second, false);
|
||||
|
||||
/*
|
||||
ROM slot mapping applied:
|
||||
@@ -113,19 +103,18 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
plus3_ = std::make_unique<Plus3>();
|
||||
|
||||
if(target.has_dfs) {
|
||||
set_rom(ROM::Slot0, *roms[dfs_rom_position], true);
|
||||
set_rom(ROM::Slot0, roms.find(::ROM::Name::Acorn1770DFS)->second, true);
|
||||
}
|
||||
if(target.has_pres_adfs) {
|
||||
set_rom(ROM::Slot4, *roms[pres_adfs_rom_position], true);
|
||||
set_rom(ROM::Slot5, *roms[pres_adfs_rom_position+1], true);
|
||||
set_rom(ROM::Slot4, roms.find(::ROM::Name::PRESADFSSlot1)->second, true);
|
||||
set_rom(ROM::Slot5, roms.find(::ROM::Name::PRESADFSSlot2)->second, true);
|
||||
}
|
||||
if(target.has_acorn_adfs) {
|
||||
set_rom(ROM::Slot6, *roms[acorn_adfs_rom_position], true);
|
||||
set_rom(ROM::Slot6, roms.find(::ROM::Name::AcornADFS)->second, true);
|
||||
}
|
||||
}
|
||||
|
||||
if(target.has_ap6_rom) {
|
||||
set_rom(ROM::Slot15, *roms[ap6_rom_position], true);
|
||||
set_rom(ROM::Slot15, roms.find(::ROM::Name::PRESAdvancedPlus6)->second, true);
|
||||
}
|
||||
|
||||
if(target.has_sideways_ram) {
|
||||
@@ -230,13 +219,15 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
if(isReadOperation(operation)) {
|
||||
*value = ram_[address];
|
||||
} else {
|
||||
if(address >= video_access_range_.low_address && address <= video_access_range_.high_address) update_display();
|
||||
if(address >= video_access_range_.low_address && address <= video_access_range_.high_address) {
|
||||
video_.flush();
|
||||
}
|
||||
ram_[address] = *value;
|
||||
}
|
||||
|
||||
// for the entire frame, RAM is accessible only on odd cycles; in modes below 4
|
||||
// it's also accessible only outside of the pixel regions
|
||||
cycles += video_output_.get_cycles_until_next_ram_availability(int(cycles_since_display_update_.as_integral()) + 1);
|
||||
// For the entire frame, RAM is accessible only on odd cycles; in modes below 4
|
||||
// it's also accessible only outside of the pixel regions.
|
||||
cycles += video_.last_valid()->get_cycles_until_next_ram_availability(video_.time_since_flush().template as<int>() + 1);
|
||||
} else {
|
||||
switch(address & 0xff0f) {
|
||||
case 0xfe00:
|
||||
@@ -274,10 +265,8 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
case 0xfe08: case 0xfe09: case 0xfe0a: case 0xfe0b:
|
||||
case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f:
|
||||
if(!isReadOperation(operation)) {
|
||||
update_display();
|
||||
video_output_.write(address, *value);
|
||||
video_access_range_ = video_output_.get_memory_access_range();
|
||||
queue_next_display_interrupt();
|
||||
video_->write(address, *value);
|
||||
video_access_range_ = video_.last_valid()->get_memory_access_range();
|
||||
}
|
||||
break;
|
||||
case 0xfe04:
|
||||
@@ -425,14 +414,14 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
(address == 0xf0a8) // 0xf0a8 is from where a service call would normally be
|
||||
// dispatched; we can check whether it would be call 14
|
||||
// (i.e. read byte) and, if so, whether the OS was about to
|
||||
// issue a read byte call to a ROM despite being the tape
|
||||
// issue a read byte call to a ROM despite the tape
|
||||
// FS being selected. If so then this is a get byte that
|
||||
// we should service synthetically. Put the byte into Y
|
||||
// and set A to zero to report that action was taken, then
|
||||
// allow the PC read to return an RTS.
|
||||
)
|
||||
) {
|
||||
uint8_t service_call = uint8_t(m6502_.get_value_of_register(CPU::MOS6502::Register::X));
|
||||
const auto service_call = uint8_t(m6502_.get_value_of_register(CPU::MOS6502::Register::X));
|
||||
if(address == 0xf0a8) {
|
||||
if(!ram_[0x247] && service_call == 14) {
|
||||
tape_.set_delegate(nullptr);
|
||||
@@ -441,7 +430,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
tape_.clear_interrupts(Interrupt::ReceiveDataFull);
|
||||
while(!tape_.get_tape()->is_at_end()) {
|
||||
tape_.run_for_input_pulse();
|
||||
cycles_left_while_plausibly_in_data--;
|
||||
--cycles_left_while_plausibly_in_data;
|
||||
if(!cycles_left_while_plausibly_in_data) fast_load_is_in_data_ = false;
|
||||
if( (tape_.get_interrupt_status() & Interrupt::ReceiveDataFull) &&
|
||||
(fast_load_is_in_data_ || tape_.get_data_register() == 0x2a)
|
||||
@@ -483,18 +472,14 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
cycles_since_display_update_ += Cycles(int(cycles));
|
||||
if(video_ += Cycles(int(cycles))) {
|
||||
signal_interrupt(video_.last_valid()->get_interrupts());
|
||||
}
|
||||
|
||||
cycles_since_audio_update_ += Cycles(int(cycles));
|
||||
if(cycles_since_audio_update_ > Cycles(16384)) update_audio();
|
||||
tape_.run_for(Cycles(int(cycles)));
|
||||
|
||||
cycles_until_display_interrupt_ -= cycles;
|
||||
if(cycles_until_display_interrupt_ < 0) {
|
||||
signal_interrupt(next_display_interrupt_);
|
||||
update_display();
|
||||
queue_next_display_interrupt();
|
||||
}
|
||||
|
||||
if(typer_) typer_->run_for(Cycles(int(cycles)));
|
||||
if(plus3_) plus3_->run_for(Cycles(4*int(cycles)));
|
||||
if(shift_restart_counter_) {
|
||||
@@ -517,25 +502,25 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
}
|
||||
|
||||
forceinline void flush() {
|
||||
update_display();
|
||||
video_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_output_.set_scan_target(scan_target);
|
||||
video_.last_valid()->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_output_.get_scaled_scan_status();
|
||||
return video_.last_valid()->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_output_.set_display_type(display_type);
|
||||
video_.last_valid()->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType get_display_type() const final {
|
||||
return video_output_.get_display_type();
|
||||
return video_.last_valid()->get_display_type();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
@@ -693,18 +678,6 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: - Work deferral updates.
|
||||
inline void update_display() {
|
||||
if(cycles_since_display_update_ > 0) {
|
||||
video_output_.run_for(cycles_since_display_update_.flush<Cycles>());
|
||||
}
|
||||
}
|
||||
|
||||
inline void queue_next_display_interrupt() {
|
||||
VideoOutput::Interrupt next_interrupt = video_output_.get_next_interrupt();
|
||||
cycles_until_display_interrupt_ = next_interrupt.cycles;
|
||||
next_display_interrupt_ = next_interrupt.interrupt;
|
||||
}
|
||||
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(SoundGenerator::clock_rate_divider)));
|
||||
}
|
||||
@@ -754,10 +727,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
Electron::KeyboardMapper keyboard_mapper_;
|
||||
|
||||
// Counters related to simultaneous subsystems
|
||||
Cycles cycles_since_display_update_ = 0;
|
||||
Cycles cycles_since_audio_update_ = 0;
|
||||
int cycles_until_display_interrupt_ = 0;
|
||||
Interrupt next_display_interrupt_ = Interrupt::RealTimeClock;
|
||||
VideoOutput::Range video_access_range_ = {0, 0xffff};
|
||||
|
||||
// Tape
|
||||
@@ -794,7 +764,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// Outputs
|
||||
VideoOutput video_output_;
|
||||
JustInTimeActor<VideoOutput, Cycles> video_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
SoundGenerator sound_generator_;
|
||||
|
||||
@@ -145,7 +145,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
#undef SHIFT
|
||||
#undef X
|
||||
|
||||
return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
|
||||
return table_lookup_sequence_for_character(key_sequences, character);
|
||||
}
|
||||
|
||||
bool CharacterMapper::needs_pause_after_key(uint16_t key) const {
|
||||
|
||||
@@ -234,7 +234,23 @@ void VideoOutput::output_pixels(int number_of_cycles) {
|
||||
|
||||
void VideoOutput::run_for(const Cycles cycles) {
|
||||
int number_of_cycles = int(cycles.as_integral());
|
||||
const auto start_position = output_position_;
|
||||
output_position_ = (output_position_ + number_of_cycles) % cycles_per_frame;
|
||||
|
||||
if(
|
||||
(start_position < real_time_clock_interrupt_1 && output_position_ >= real_time_clock_interrupt_1) ||
|
||||
(start_position < real_time_clock_interrupt_2 && output_position_ >= real_time_clock_interrupt_2)
|
||||
) {
|
||||
interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::RealTimeClock);
|
||||
}
|
||||
|
||||
if(
|
||||
(start_position < display_end_interrupt_1 && output_position_ >= display_end_interrupt_1) ||
|
||||
(start_position < display_end_interrupt_2 && output_position_ >= display_end_interrupt_2)
|
||||
) {
|
||||
interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::DisplayEnd);
|
||||
}
|
||||
|
||||
while(number_of_cycles) {
|
||||
int draw_action_length = screen_map_[screen_map_pointer_].length;
|
||||
int time_left_in_action = std::min(number_of_cycles, draw_action_length - cycles_into_draw_action_);
|
||||
@@ -354,36 +370,30 @@ void VideoOutput::setup_base_address() {
|
||||
|
||||
// MARK: - Interrupts
|
||||
|
||||
VideoOutput::Interrupt VideoOutput::get_next_interrupt() {
|
||||
VideoOutput::Interrupt interrupt;
|
||||
|
||||
Cycles VideoOutput::get_next_sequence_point() {
|
||||
if(output_position_ < real_time_clock_interrupt_1) {
|
||||
interrupt.cycles = real_time_clock_interrupt_1 - output_position_;
|
||||
interrupt.interrupt = RealTimeClock;
|
||||
return interrupt;
|
||||
return real_time_clock_interrupt_1 - output_position_;
|
||||
}
|
||||
|
||||
if(output_position_ < display_end_interrupt_1) {
|
||||
interrupt.cycles = display_end_interrupt_1 - output_position_;
|
||||
interrupt.interrupt = DisplayEnd;
|
||||
return interrupt;
|
||||
return display_end_interrupt_1 - output_position_;
|
||||
}
|
||||
|
||||
if(output_position_ < real_time_clock_interrupt_2) {
|
||||
interrupt.cycles = real_time_clock_interrupt_2 - output_position_;
|
||||
interrupt.interrupt = RealTimeClock;
|
||||
return interrupt;
|
||||
return real_time_clock_interrupt_2 - output_position_;
|
||||
}
|
||||
|
||||
if(output_position_ < display_end_interrupt_2) {
|
||||
interrupt.cycles = display_end_interrupt_2 - output_position_;
|
||||
interrupt.interrupt = DisplayEnd;
|
||||
return interrupt;
|
||||
return display_end_interrupt_2 - output_position_;
|
||||
}
|
||||
|
||||
interrupt.cycles = real_time_clock_interrupt_1 + cycles_per_frame - output_position_;
|
||||
interrupt.interrupt = RealTimeClock;
|
||||
return interrupt;
|
||||
return real_time_clock_interrupt_1 + cycles_per_frame - output_position_;
|
||||
}
|
||||
|
||||
Electron::Interrupt VideoOutput::get_interrupts() {
|
||||
const auto interrupts = interrupts_;
|
||||
interrupts_ = Electron::Interrupt(0);
|
||||
return interrupts;
|
||||
}
|
||||
|
||||
// MARK: - RAM timing and access information
|
||||
|
||||
@@ -54,15 +54,6 @@ class VideoOutput {
|
||||
*/
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*!
|
||||
Describes an interrupt the video hardware will generate by its identity and scheduling time.
|
||||
*/
|
||||
struct Interrupt {
|
||||
/// The interrupt that will be signalled.
|
||||
Electron::Interrupt interrupt;
|
||||
/// The number of cycles until it is signalled.
|
||||
int cycles;
|
||||
};
|
||||
/*!
|
||||
@returns the next interrupt that should be generated as a result of the video hardware.
|
||||
The time until signalling returned is the number of cycles after the final one triggered
|
||||
@@ -70,7 +61,12 @@ class VideoOutput {
|
||||
|
||||
This result may be mutated by calls to @c write.
|
||||
*/
|
||||
Interrupt get_next_interrupt();
|
||||
Cycles get_next_sequence_point();
|
||||
|
||||
/*!
|
||||
@returns a bit mask of all interrupts that have been triggered since the last call to get_interrupt().
|
||||
*/
|
||||
Electron::Interrupt get_interrupts();
|
||||
|
||||
/*!
|
||||
@returns the number of cycles after (final cycle of last run_for batch + @c from_time)
|
||||
@@ -136,6 +132,8 @@ class VideoOutput {
|
||||
void emplace_pixel_line();
|
||||
std::size_t screen_map_pointer_ = 0;
|
||||
int cycles_into_draw_action_ = 0;
|
||||
|
||||
Electron::Interrupt interrupts_ = Electron::Interrupt(0);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
337
Machines/Enterprise/Dave.cpp
Normal file
337
Machines/Enterprise/Dave.cpp
Normal file
@@ -0,0 +1,337 @@
|
||||
//
|
||||
// Dave.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 22/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Dave.hpp"
|
||||
|
||||
using namespace Enterprise::Dave;
|
||||
|
||||
// MARK: - Audio generator
|
||||
|
||||
Audio::Audio(Concurrency::DeferringAsyncTaskQueue &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
void Audio::write(uint16_t address, uint8_t value) {
|
||||
address &= 0x1f;
|
||||
audio_queue_.defer([address, value, this] {
|
||||
switch(address) {
|
||||
case 0: case 2: case 4:
|
||||
channels_[address >> 1].reload = (channels_[address >> 1].reload & 0xff00) | value;
|
||||
break;
|
||||
case 1: case 3: case 5:
|
||||
channels_[address >> 1].reload = uint16_t((channels_[address >> 1].reload & 0x00ff) | ((value & 0xf) << 8));
|
||||
channels_[address >> 1].distortion = Channel::Distortion((value >> 4)&3);
|
||||
channels_[address >> 1].high_pass = value & 0x40;
|
||||
channels_[address >> 1].ring_modulate = value & 0x80;
|
||||
break;
|
||||
case 6:
|
||||
noise_.frequency = Noise::Frequency(value&3);
|
||||
noise_.polynomial = Noise::Polynomial((value >> 2)&3);
|
||||
noise_.swap_polynomial = value & 0x10;
|
||||
noise_.low_pass = value & 0x20;
|
||||
noise_.high_pass = value & 0x40;
|
||||
noise_.ring_modulate = value & 0x80;
|
||||
break;
|
||||
|
||||
case 7:
|
||||
channels_[0].sync = value & 0x01;
|
||||
channels_[1].sync = value & 0x02;
|
||||
channels_[2].sync = value & 0x04;
|
||||
use_direct_output_[0] = value & 0x08;
|
||||
use_direct_output_[1] = value & 0x10;
|
||||
// Interrupt bits are handled separately.
|
||||
break;
|
||||
|
||||
case 8: case 9: case 10:
|
||||
channels_[address - 8].amplitude[0] = value & 0x3f;
|
||||
break;
|
||||
case 12: case 13: case 14:
|
||||
channels_[address - 12].amplitude[1] = value & 0x3f;
|
||||
break;
|
||||
case 11: noise_.amplitude[0] = value & 0x3f; break;
|
||||
case 15: noise_.amplitude[1] = value & 0x3f; break;
|
||||
|
||||
case 31:
|
||||
global_divider_reload_ = 2 + ((value >> 1)&1);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Audio::set_sample_volume_range(int16_t range) {
|
||||
audio_queue_.defer([range, this] {
|
||||
volume_ = range / (63*4);
|
||||
});
|
||||
}
|
||||
|
||||
void Audio::update_channel(int c) {
|
||||
auto output = channels_[c].output & 1;
|
||||
channels_[c].output <<= 1;
|
||||
if(channels_[c].sync) {
|
||||
channels_[c].count = channels_[c].reload;
|
||||
output = 0;
|
||||
} else {
|
||||
if(!channels_[c].count) {
|
||||
channels_[c].count = channels_[c].reload;
|
||||
|
||||
if(channels_[c].distortion == Channel::Distortion::None)
|
||||
output ^= 1;
|
||||
else
|
||||
output = poly_state_[int(channels_[c].distortion)];
|
||||
} else {
|
||||
--channels_[c].count;
|
||||
}
|
||||
|
||||
if(channels_[c].high_pass && (channels_[(c+1)%3].output&3) == 2) {
|
||||
output = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Ring modulation applies even when sync is enabled, per SIDBasic.
|
||||
if(channels_[c].ring_modulate) {
|
||||
output = ~(output ^ channels_[(c+2)%3].output) & 1;
|
||||
}
|
||||
|
||||
channels_[c].output |= output;
|
||||
}
|
||||
|
||||
void Audio::get_samples(std::size_t number_of_samples, int16_t *target) {
|
||||
int16_t output_level[2];
|
||||
size_t c = 0;
|
||||
while(c < number_of_samples) {
|
||||
// I'm unclear on the details of the time division multiplexing so,
|
||||
// for now, just sum the outputs.
|
||||
output_level[0] =
|
||||
volume_ *
|
||||
(use_direct_output_[0] ?
|
||||
channels_[0].amplitude[0]
|
||||
: (
|
||||
channels_[0].amplitude[0] * (channels_[0].output & 1) +
|
||||
channels_[1].amplitude[0] * (channels_[1].output & 1) +
|
||||
channels_[2].amplitude[0] * (channels_[2].output & 1) +
|
||||
noise_.amplitude[0] * noise_.final_output
|
||||
));
|
||||
|
||||
output_level[1] =
|
||||
volume_ *
|
||||
(use_direct_output_[1] ?
|
||||
channels_[0].amplitude[1]
|
||||
: (
|
||||
channels_[0].amplitude[1] * (channels_[0].output & 1) +
|
||||
channels_[1].amplitude[1] * (channels_[1].output & 1) +
|
||||
channels_[2].amplitude[1] * (channels_[2].output & 1) +
|
||||
noise_.amplitude[1] * noise_.final_output
|
||||
));
|
||||
|
||||
while(global_divider_ && c < number_of_samples) {
|
||||
--global_divider_;
|
||||
*reinterpret_cast<uint32_t *>(&target[c << 1]) = *reinterpret_cast<uint32_t *>(output_level);
|
||||
++c;
|
||||
}
|
||||
|
||||
global_divider_ = global_divider_reload_;
|
||||
if(!global_divider_) {
|
||||
global_divider_ = global_divider_reload_;
|
||||
}
|
||||
poly_state_[int(Channel::Distortion::FourBit)] = poly4_.next();
|
||||
poly_state_[int(Channel::Distortion::FiveBit)] = poly5_.next();
|
||||
poly_state_[int(Channel::Distortion::SevenBit)] = poly7_.next();
|
||||
if(noise_.swap_polynomial) {
|
||||
poly_state_[int(Channel::Distortion::SevenBit)] = poly_state_[int(Channel::Distortion::None)];
|
||||
}
|
||||
|
||||
// Update tone channels.
|
||||
update_channel(0);
|
||||
update_channel(1);
|
||||
update_channel(2);
|
||||
|
||||
// Update noise channel.
|
||||
|
||||
// Step 1: decide whether there is a tick to apply.
|
||||
bool noise_tick = false;
|
||||
if(noise_.frequency == Noise::Frequency::DivideByFour) {
|
||||
if(!noise_.count) {
|
||||
noise_tick = true;
|
||||
noise_.count = 3;
|
||||
} else {
|
||||
--noise_.count;
|
||||
}
|
||||
} else {
|
||||
noise_tick = (channels_[int(noise_.frequency) - 1].output&3) == 2;
|
||||
}
|
||||
|
||||
// Step 2: tick if necessary.
|
||||
int noise_output = noise_.output & 1;
|
||||
noise_.output <<= 1;
|
||||
if(noise_tick) {
|
||||
switch(noise_.polynomial) {
|
||||
case Noise::Polynomial::SeventeenBit:
|
||||
poly_state_[int(Channel::Distortion::None)] = uint8_t(poly17_.next());
|
||||
break;
|
||||
case Noise::Polynomial::FifteenBit:
|
||||
poly_state_[int(Channel::Distortion::None)] = uint8_t(poly15_.next());
|
||||
break;
|
||||
case Noise::Polynomial::ElevenBit:
|
||||
poly_state_[int(Channel::Distortion::None)] = uint8_t(poly11_.next());
|
||||
break;
|
||||
case Noise::Polynomial::NineBit:
|
||||
poly_state_[int(Channel::Distortion::None)] = uint8_t(poly9_.next());
|
||||
break;
|
||||
}
|
||||
|
||||
noise_output = poly_state_[int(Channel::Distortion::None)];
|
||||
}
|
||||
noise_.output |= noise_output;
|
||||
|
||||
// Low pass: sample channel 2 on downward transitions of the prima facie output.
|
||||
if(noise_.low_pass && (noise_.output & 3) == 2) {
|
||||
noise_.output = (noise_.output & ~1) | (channels_[2].output & 1);
|
||||
}
|
||||
|
||||
// Apply noise high-pass.
|
||||
if(noise_.high_pass && (channels_[0].output & 3) == 2) {
|
||||
noise_.output &= ~1;
|
||||
}
|
||||
|
||||
// Update noise ring modulation, if any.
|
||||
if(noise_.ring_modulate) {
|
||||
noise_.final_output = !((noise_.output ^ channels_[1].output) & 1);
|
||||
} else {
|
||||
noise_.final_output = noise_.output & 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interrupt source
|
||||
|
||||
uint8_t TimedInterruptSource::get_new_interrupts() {
|
||||
const uint8_t result = interrupts_;
|
||||
interrupts_ = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
void TimedInterruptSource::write(uint16_t address, uint8_t value) {
|
||||
address &= 0x1f;
|
||||
switch(address) {
|
||||
default: break;
|
||||
|
||||
case 0: case 2:
|
||||
channels_[address >> 1].reload = (channels_[address >> 1].reload & 0xff00) | value;
|
||||
break;
|
||||
case 1: case 3:
|
||||
channels_[address >> 1].reload = uint16_t((channels_[address >> 1].reload & 0x00ff) | ((value & 0xf) << 8));
|
||||
break;
|
||||
|
||||
case 7:
|
||||
channels_[0].sync = value & 0x01;
|
||||
channels_[1].sync = value & 0x02;
|
||||
rate_ = InterruptRate((value >> 5) & 3);
|
||||
break;
|
||||
|
||||
case 31:
|
||||
global_divider_ = Cycles(2 + ((value >> 1)&1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TimedInterruptSource::update_channel(int c, bool is_linked, int decrement) {
|
||||
if(channels_[c].sync) {
|
||||
channels_[c].value = channels_[c].reload;
|
||||
} else {
|
||||
if(decrement <= channels_[c].value) {
|
||||
channels_[c].value -= decrement;
|
||||
} else {
|
||||
// The decrement is greater than the current value, therefore
|
||||
// there'll be at least one flip.
|
||||
//
|
||||
// After decreasing the decrement by the current value + 1,
|
||||
// it'll be clear how many decrements are left after reload.
|
||||
//
|
||||
// Dividing that by the number of decrements necessary for a
|
||||
// flip will provide the total number of flips.
|
||||
const int decrements_after_flip = decrement - (channels_[c].value + 1);
|
||||
const int num_flips = 1 + decrements_after_flip / (channels_[c].reload + 1);
|
||||
|
||||
// If this is a linked channel, set the interrupt mask if a transition
|
||||
// from high to low is amongst the included flips.
|
||||
if(is_linked && num_flips + channels_[c].level >= 2) {
|
||||
interrupts_ |= uint8_t(Interrupt::VariableFrequency);
|
||||
programmable_level_ ^= true;
|
||||
}
|
||||
channels_[c].level ^= (num_flips & 1);
|
||||
|
||||
// Apply the modulo number of decrements to the reload value to
|
||||
// figure out where things stand now.
|
||||
channels_[c].value = channels_[c].reload - decrements_after_flip % (channels_[c].reload + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TimedInterruptSource::run_for(Cycles duration) {
|
||||
// Determine total number of ticks.
|
||||
run_length_ += duration;
|
||||
const Cycles cycles = run_length_.divide(global_divider_);
|
||||
if(cycles == Cycles(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the two-second counter, from which the 1Hz, 50Hz and 1000Hz signals
|
||||
// are derived.
|
||||
const int previous_counter = two_second_counter_;
|
||||
two_second_counter_ = (two_second_counter_ + cycles.as<int>()) % 500'000;
|
||||
|
||||
// Check for a 1Hz rollover.
|
||||
if(previous_counter / 250'000 != two_second_counter_ / 250'000) {
|
||||
interrupts_ |= uint8_t(Interrupt::OneHz);
|
||||
}
|
||||
|
||||
// Check for 1kHz or 50Hz rollover;
|
||||
switch(rate_) {
|
||||
default: break;
|
||||
case InterruptRate::OnekHz:
|
||||
if(previous_counter / 250 != two_second_counter_ / 250) {
|
||||
interrupts_ |= uint8_t(Interrupt::VariableFrequency);
|
||||
programmable_level_ ^= true;
|
||||
}
|
||||
break;
|
||||
case InterruptRate::FiftyHz:
|
||||
if(previous_counter / 5'000 != two_second_counter_ / 5'000) {
|
||||
interrupts_ |= uint8_t(Interrupt::VariableFrequency);
|
||||
programmable_level_ ^= true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Update the two tone channels.
|
||||
update_channel(0, rate_ == InterruptRate::ToneGenerator0, cycles.as<int>());
|
||||
update_channel(1, rate_ == InterruptRate::ToneGenerator1, cycles.as<int>());
|
||||
}
|
||||
|
||||
Cycles TimedInterruptSource::get_next_sequence_point() const {
|
||||
// Since both the 1kHz and 50Hz timers are integer dividers of the 1Hz timer, there's no need
|
||||
// to factor that one in when determining the next sequence point for either of those.
|
||||
switch(rate_) {
|
||||
default:
|
||||
case InterruptRate::OnekHz: return Cycles(250 - (two_second_counter_ % 250));
|
||||
case InterruptRate::FiftyHz: return Cycles(5000 - (two_second_counter_ % 5000));
|
||||
|
||||
case InterruptRate::ToneGenerator0:
|
||||
case InterruptRate::ToneGenerator1: {
|
||||
const auto &channel = channels_[int(rate_) - int(InterruptRate::ToneGenerator0)];
|
||||
const int cycles_until_interrupt = channel.value + 1 + (!channel.level) * (channel.reload + 1);
|
||||
|
||||
return Cycles(std::min(
|
||||
250'000 - (two_second_counter_ % 250'000),
|
||||
cycles_until_interrupt
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t TimedInterruptSource::get_divider_state() {
|
||||
return uint8_t((two_second_counter_ / 250'000) * 4 | programmable_level_);
|
||||
}
|
||||
188
Machines/Enterprise/Dave.hpp
Normal file
188
Machines/Enterprise/Dave.hpp
Normal file
@@ -0,0 +1,188 @@
|
||||
//
|
||||
// Dave.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 22/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Dave_hpp
|
||||
#define Dave_hpp
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "../../Numeric/LFSR.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
|
||||
namespace Enterprise {
|
||||
namespace Dave {
|
||||
|
||||
enum class Interrupt: uint8_t {
|
||||
VariableFrequency = 0x02,
|
||||
OneHz = 0x08,
|
||||
Nick = 0x20,
|
||||
};
|
||||
|
||||
/*!
|
||||
Models the audio-production subset of Dave's behaviour.
|
||||
*/
|
||||
class Audio: public Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
Audio(Concurrency::DeferringAsyncTaskQueue &audio_queue);
|
||||
|
||||
/// Modifies an register in the audio range; only the low 4 bits are
|
||||
/// used for register decoding so it's assumed that the caller has
|
||||
/// already identified this write as being to an audio register.
|
||||
void write(uint16_t address, uint8_t value);
|
||||
|
||||
// MARK: - SampleSource.
|
||||
void set_sample_volume_range(int16_t range);
|
||||
static constexpr bool get_is_stereo() { return true; } // Dave produces stereo sound.
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
|
||||
private:
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
||||
// Global divider (i.e. 8MHz/12Mhz switch).
|
||||
uint8_t global_divider_;
|
||||
uint8_t global_divider_reload_ = 2;
|
||||
|
||||
// Tone channels.
|
||||
struct Channel {
|
||||
// User-set values.
|
||||
uint16_t reload = 0;
|
||||
bool high_pass = false;
|
||||
bool ring_modulate = false;
|
||||
enum class Distortion {
|
||||
None = 0,
|
||||
FourBit = 1,
|
||||
FiveBit = 2,
|
||||
SevenBit = 3,
|
||||
} distortion = Distortion::None;
|
||||
uint8_t amplitude[2]{};
|
||||
bool sync = false;
|
||||
|
||||
// Current state.
|
||||
uint16_t count = 0;
|
||||
int output = 0;
|
||||
} channels_[3];
|
||||
void update_channel(int);
|
||||
|
||||
// Noise channel.
|
||||
struct Noise {
|
||||
// User-set values.
|
||||
uint8_t amplitude[2]{};
|
||||
enum class Frequency {
|
||||
DivideByFour,
|
||||
ToneChannel0,
|
||||
ToneChannel1,
|
||||
ToneChannel2,
|
||||
} frequency = Frequency::DivideByFour;
|
||||
enum class Polynomial {
|
||||
SeventeenBit,
|
||||
FifteenBit,
|
||||
ElevenBit,
|
||||
NineBit
|
||||
} polynomial = Polynomial::SeventeenBit;
|
||||
bool swap_polynomial = false;
|
||||
bool low_pass = false;
|
||||
bool high_pass = false;
|
||||
bool ring_modulate = false;
|
||||
|
||||
// Current state.
|
||||
int count = 0;
|
||||
int output = 0;
|
||||
bool final_output = false;
|
||||
} noise_;
|
||||
void update_noise();
|
||||
|
||||
bool use_direct_output_[2]{};
|
||||
|
||||
// Global volume, per SampleSource obligations.
|
||||
int16_t volume_ = 0;
|
||||
|
||||
// Polynomials that are always running.
|
||||
Numeric::LFSRv<0xc> poly4_;
|
||||
Numeric::LFSRv<0x14> poly5_;
|
||||
Numeric::LFSRv<0x60> poly7_;
|
||||
|
||||
// The selectable, noise-related polynomial.
|
||||
Numeric::LFSRv<0x110> poly9_;
|
||||
Numeric::LFSRv<0x500> poly11_;
|
||||
Numeric::LFSRv<0x6000> poly15_;
|
||||
Numeric::LFSRv<0x12000> poly17_;
|
||||
|
||||
// Current state of the active polynomials.
|
||||
uint8_t poly_state_[4];
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides Dave's timed interrupts — those that are provided at 1 kHz,
|
||||
50 Hz or according to the rate of tone generators 0 or 1, plus the fixed
|
||||
1 Hz interrupt.
|
||||
*/
|
||||
class TimedInterruptSource {
|
||||
public:
|
||||
/// Modifies an register in the audio range; only the low 4 bits are
|
||||
/// used for register decoding so it's assumed that the caller has
|
||||
/// already identified this write as being to an audio register.
|
||||
void write(uint16_t address, uint8_t value);
|
||||
|
||||
/// Returns a bitmask of interrupts that have become active since
|
||||
/// the last time this method was called; flags are as defined in
|
||||
/// @c Enterprise::Dave::Interrupt
|
||||
uint8_t get_new_interrupts();
|
||||
|
||||
/// Returns the current high or low states of the inputs that trigger
|
||||
/// the interrupts modelled here, as a bit mask compatible with that
|
||||
/// exposed by Dave as the register at 0xb4.
|
||||
uint8_t get_divider_state();
|
||||
|
||||
/// Advances the interrupt source.
|
||||
void run_for(Cycles);
|
||||
|
||||
/// @returns The amount of time from now until the earliest that
|
||||
/// @c get_new_interrupts() _might_ have new interrupts to report.
|
||||
Cycles get_next_sequence_point() const;
|
||||
|
||||
private:
|
||||
static constexpr Cycles clock_rate{250000};
|
||||
static constexpr Cycles half_clock_rate{125000};
|
||||
|
||||
// Global divider (i.e. 8MHz/12Mhz switch).
|
||||
Cycles global_divider_ = Cycles(2);
|
||||
Cycles run_length_;
|
||||
|
||||
// Interrupts that have fired since get_new_interrupts()
|
||||
// was last called.
|
||||
uint8_t interrupts_ = 0;
|
||||
|
||||
// A counter for the 1Hz interrupt.
|
||||
int two_second_counter_ = 0;
|
||||
|
||||
// A counter specific to the 1kHz and 50Hz timers, if in use.
|
||||
enum class InterruptRate {
|
||||
OnekHz,
|
||||
FiftyHz,
|
||||
ToneGenerator0,
|
||||
ToneGenerator1,
|
||||
} rate_ = InterruptRate::OnekHz;
|
||||
bool programmable_level_ = false;
|
||||
|
||||
// A local duplicate of the counting state of the first two audio
|
||||
// channels, maintained in case either of those is used as an
|
||||
// interrupt source.
|
||||
struct Channel {
|
||||
int value = 100, reload = 100;
|
||||
bool sync = false;
|
||||
bool level = false;
|
||||
} channels_[2];
|
||||
void update_channel(int c, bool is_linked, int decrement);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Dave_hpp */
|
||||
78
Machines/Enterprise/EXDos.cpp
Normal file
78
Machines/Enterprise/EXDos.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// EXDos.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "EXDos.hpp"
|
||||
|
||||
// TODO: disk_did_change_ should be on the drive. Some drives report it.
|
||||
|
||||
using namespace Enterprise;
|
||||
|
||||
EXDos::EXDos() : WD1770(P1770) {
|
||||
emplace_drives(4, 8000000, 300, 2);
|
||||
set_control_register(0x00);
|
||||
}
|
||||
|
||||
void EXDos::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
|
||||
get_drive(drive).set_disk(disk);
|
||||
disk_did_change_ = true;
|
||||
}
|
||||
|
||||
// Documentation for the control register:
|
||||
//
|
||||
// Write:
|
||||
// b7 in use (???)
|
||||
// b6 disk change reset
|
||||
// b5 0 = double density, 1 = single density
|
||||
// b4 side 1 select
|
||||
// b3, b3, b1, b0 select drive 3, 2, 1, 0
|
||||
//
|
||||
// Read:
|
||||
// b7 data request from WD1770
|
||||
// b6 disk change
|
||||
// b5, b4, b3, b2: not used
|
||||
// b1 interrupt request from WD1770
|
||||
// b0 drive ready
|
||||
|
||||
void EXDos::set_control_register(uint8_t control) {
|
||||
if(control & 0x40) disk_did_change_ = false;
|
||||
set_is_double_density(!(control & 0x20));
|
||||
|
||||
// Set side.
|
||||
const int head = (control >> 4) & 1;
|
||||
for(size_t c = 0; c < 4; c++) {
|
||||
get_drive(c).set_head(head);
|
||||
}
|
||||
|
||||
// Select drive, ensuring handover of the motor-on state.
|
||||
const bool motor_state = get_drive().get_motor_on();
|
||||
for_all_drives([] (Storage::Disk::Drive &drive, size_t) {
|
||||
drive.set_motor_on(false);
|
||||
});
|
||||
set_drive(control & 0xf);
|
||||
get_drive().set_motor_on(motor_state);
|
||||
}
|
||||
|
||||
uint8_t EXDos::get_control_register() {
|
||||
const uint8_t status =
|
||||
(get_data_request_line() ? 0x80 : 0x00) |
|
||||
(disk_did_change_ ? 0x40 : 0x00) |
|
||||
(get_interrupt_request_line() ? 0x02 : 0x00) |
|
||||
(get_drive().get_is_ready() ? 0x01 : 0x00);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void EXDos::set_motor_on(bool on) {
|
||||
get_drive().set_motor_on(on);
|
||||
}
|
||||
|
||||
void EXDos::set_activity_observer(Activity::Observer *observer) {
|
||||
for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) {
|
||||
drive.set_activity_observer(observer, "Drive " + std::to_string(index+1), true);
|
||||
});
|
||||
}
|
||||
36
Machines/Enterprise/EXDos.hpp
Normal file
36
Machines/Enterprise/EXDos.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// EXDos.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 20/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef EXDos_hpp
|
||||
#define EXDos_hpp
|
||||
|
||||
#include "../../Components/1770/1770.hpp"
|
||||
#include "../../Activity/Observer.hpp"
|
||||
|
||||
namespace Enterprise {
|
||||
|
||||
class EXDos final : public WD::WD1770 {
|
||||
public:
|
||||
EXDos();
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
|
||||
|
||||
void set_control_register(uint8_t control);
|
||||
uint8_t get_control_register();
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
|
||||
private:
|
||||
bool disk_did_change_ = false;
|
||||
|
||||
void set_motor_on(bool on) override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* EXDos_hpp */
|
||||
765
Machines/Enterprise/Enterprise.cpp
Normal file
765
Machines/Enterprise/Enterprise.cpp
Normal file
@@ -0,0 +1,765 @@
|
||||
//
|
||||
// Enterprise.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 10/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Enterprise.hpp"
|
||||
|
||||
#include "Dave.hpp"
|
||||
#include "EXDos.hpp"
|
||||
#include "Keyboard.hpp"
|
||||
#include "Nick.hpp"
|
||||
|
||||
#include "../MachineTypes.hpp"
|
||||
#include "../Utility/Typer.hpp"
|
||||
|
||||
#include "../../Analyser/Static/Enterprise/Target.hpp"
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../Processors/Z80/Z80.hpp"
|
||||
|
||||
#define LOG_PREFIX "[Enterprise] "
|
||||
#include "../../Outputs/Log.hpp"
|
||||
|
||||
namespace Enterprise {
|
||||
|
||||
/*
|
||||
Notes to self on timing:
|
||||
|
||||
Nick divides each line into 57 windows; each window lasts 16 cycles and dedicates the
|
||||
first 10 of those to VRAM accesses, leaving the final six for a Z80 video RAM access
|
||||
if one has been requested.
|
||||
|
||||
The Z80 has a separate, asynchronous 4Mhz clock. That's that.
|
||||
|
||||
The documentation is also very forward in emphasising that Nick generates phaselocked
|
||||
(i.e. in-phase) PAL video.
|
||||
|
||||
So: 57*16 = 912 cycles/line.
|
||||
|
||||
A standard PAL line lasts 64µs and during that time outputs 283.7516 colour cycles.
|
||||
|
||||
I shall _guess_ that the Enterprise stretches each line to 284 colour cycles rather than
|
||||
reducing it to 283.
|
||||
|
||||
Therefore 912 cycles occurs in 284/283.7516 * 64 µs.
|
||||
|
||||
So one line = 181760000 / 2837516 µs = 45440000 / 709379 µs
|
||||
=> one cycle = 45440000 / 709379*912 = 45440000 / 646953648 = 2840000 / 40434603 µs
|
||||
=> clock rate of 40434603 / 2840000 Mhz
|
||||
|
||||
And, therefore, the ratio to a 4Mhz Z80 clock is:
|
||||
|
||||
40434603 / (2840000 * 4)
|
||||
= 40434603 / 11360000
|
||||
i.e. roughly 3.55 Nick cycles per Z80 cycle.
|
||||
|
||||
If that's true then the 6-cycle window is around 1.69 Z80 cycles long. Given that the Z80
|
||||
clock in an Enterprise can be stopped in half-cycle increments only, the Z80 can only be
|
||||
guaranteed to have around a 1.19 cycle minimum for its actual access. I'm therefore further
|
||||
postulating that the clock stoppage takes place so as to align the final cycle of a relevant
|
||||
access over the available window.
|
||||
|
||||
*/
|
||||
|
||||
template <bool has_disk_controller, bool is_6mhz> class ConcreteMachine:
|
||||
public Activity::Source,
|
||||
public Configurable::Device,
|
||||
public CPU::Z80::BusHandler,
|
||||
public Machine,
|
||||
public MachineTypes::AudioProducer,
|
||||
public MachineTypes::MappedKeyboardMachine,
|
||||
public MachineTypes::MediaTarget,
|
||||
public MachineTypes::ScanProducer,
|
||||
public MachineTypes::TimedMachine,
|
||||
public Utility::TypeRecipient<CharacterMapper> {
|
||||
private:
|
||||
constexpr uint8_t min_ram_slot(const Analyser::Static::Enterprise::Target &target) {
|
||||
size_t ram_size = 128*1024;
|
||||
switch(target.model) {
|
||||
case Analyser::Static::Enterprise::Target::Model::Enterprise64: ram_size = 64*1024; break;
|
||||
case Analyser::Static::Enterprise::Target::Model::Enterprise128: ram_size = 128*1024; break;
|
||||
case Analyser::Static::Enterprise::Target::Model::Enterprise256: ram_size = 256*1024; break;
|
||||
}
|
||||
|
||||
return uint8_t(0x100 - ram_size / 0x4000);
|
||||
}
|
||||
|
||||
static constexpr double clock_rate = is_6mhz ? 6'000'000.0 : 4'000'000.0;
|
||||
using NickType =
|
||||
std::conditional_t<is_6mhz,
|
||||
JustInTimeActor<Nick, HalfCycles, 13478201, 5680000>,
|
||||
JustInTimeActor<Nick, HalfCycles, 40434603, 11360000>>;
|
||||
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::Enterprise::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
min_ram_slot_(min_ram_slot(target)),
|
||||
z80_(*this),
|
||||
nick_(ram_.end() - 65536),
|
||||
dave_audio_(audio_queue_),
|
||||
speaker_(dave_audio_) {
|
||||
|
||||
// Request a clock of 4Mhz; this'll be mapped upwards for Nick and downwards for Dave elsewhere.
|
||||
set_clock_rate(clock_rate);
|
||||
speaker_.set_input_rate(float(clock_rate) / float(dave_divider));
|
||||
|
||||
ROM::Request request;
|
||||
using Target = Analyser::Static::Enterprise::Target;
|
||||
|
||||
// Pick one or more EXOS ROMs.
|
||||
switch(target.exos_version) {
|
||||
case Target::EXOSVersion::v10: request = request && ROM::Request(ROM::Name::EnterpriseEXOS10); break;
|
||||
case Target::EXOSVersion::v20: request = request && ROM::Request(ROM::Name::EnterpriseEXOS20); break;
|
||||
case Target::EXOSVersion::v21: request = request && ROM::Request(ROM::Name::EnterpriseEXOS21); break;
|
||||
case Target::EXOSVersion::v23: request = request && ROM::Request(ROM::Name::EnterpriseEXOS23); break;
|
||||
case Target::EXOSVersion::Any:
|
||||
request =
|
||||
request && (
|
||||
ROM::Request(ROM::Name::EnterpriseEXOS10) || ROM::Request(ROM::Name::EnterpriseEXOS20) ||
|
||||
ROM::Request(ROM::Name::EnterpriseEXOS21) || ROM::Request(ROM::Name::EnterpriseEXOS23)
|
||||
);
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Similarly pick one or more BASIC ROMs.
|
||||
switch(target.basic_version) {
|
||||
case Target::BASICVersion::v10:
|
||||
request = request && (
|
||||
ROM::Request(ROM::Name::EnterpriseBASIC10) ||
|
||||
(ROM::Request(ROM::Name::EnterpriseBASIC10Part1) && ROM::Request(ROM::Name::EnterpriseBASIC10Part2))
|
||||
);
|
||||
break;
|
||||
case Target::BASICVersion::v11:
|
||||
request = request && (
|
||||
ROM::Request(ROM::Name::EnterpriseBASIC11) ||
|
||||
ROM::Request(ROM::Name::EnterpriseBASIC11Suffixed)
|
||||
);
|
||||
case Target::BASICVersion::v21:
|
||||
request = request && ROM::Request(ROM::Name::EnterpriseBASIC21);
|
||||
break;
|
||||
case Target::BASICVersion::Any:
|
||||
request =
|
||||
request && (
|
||||
ROM::Request(ROM::Name::EnterpriseBASIC10) ||
|
||||
(ROM::Request(ROM::Name::EnterpriseBASIC10Part1) && ROM::Request(ROM::Name::EnterpriseBASIC10Part2)) ||
|
||||
ROM::Request(ROM::Name::EnterpriseBASIC11) ||
|
||||
ROM::Request(ROM::Name::EnterpriseBASIC21)
|
||||
);
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Possibly add in a DOS.
|
||||
switch(target.dos) {
|
||||
case Target::DOS::EXDOS: request = request && ROM::Request(ROM::Name::EnterpriseEXDOS); break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Get and validate ROMs.
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
// Extract the appropriate EXOS ROM.
|
||||
exos_.fill(0xff);
|
||||
for(const auto rom_name: { ROM::Name::EnterpriseEXOS10, ROM::Name::EnterpriseEXOS20, ROM::Name::EnterpriseEXOS21, ROM::Name::EnterpriseEXOS23 }) {
|
||||
const auto exos = roms.find(rom_name);
|
||||
if(exos != roms.end()) {
|
||||
memcpy(exos_.data(), exos->second.data(), std::min(exos_.size(), exos->second.size()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the appropriate BASIC ROM[s] (if any).
|
||||
basic_.fill(0xff);
|
||||
bool has_basic = false;
|
||||
for(const auto rom_name: { ROM::Name::EnterpriseBASIC10, ROM::Name::EnterpriseBASIC11, ROM::Name::EnterpriseBASIC11Suffixed, ROM::Name::EnterpriseBASIC21 }) {
|
||||
const auto basic = roms.find(rom_name);
|
||||
if(basic != roms.end()) {
|
||||
memcpy(basic_.data(), basic->second.data(), std::min(basic_.size(), basic->second.size()));
|
||||
has_basic = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!has_basic) {
|
||||
const auto basic1 = roms.find(ROM::Name::EnterpriseBASIC10Part1);
|
||||
const auto basic2 = roms.find(ROM::Name::EnterpriseBASIC10Part2);
|
||||
if(basic1 != roms.end() && basic2 != roms.end()) {
|
||||
memcpy(&basic_[0x0000], basic1->second.data(), std::min(size_t(8192), basic1->second.size()));
|
||||
memcpy(&basic_[0x2000], basic2->second.data(), std::min(size_t(8192), basic2->second.size()));
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the appropriate DOS ROMs.
|
||||
epdos_rom_.fill(0xff);
|
||||
const auto epdos = roms.find(ROM::Name::EnterpriseEPDOS);
|
||||
if(epdos != roms.end()) {
|
||||
memcpy(epdos_rom_.data(), epdos->second.data(), std::min(epdos_rom_.size(), epdos->second.size()));
|
||||
}
|
||||
exdos_rom_.fill(0xff);
|
||||
const auto exdos = roms.find(ROM::Name::EnterpriseEXDOS);
|
||||
if(exdos != roms.end()) {
|
||||
memcpy(exdos_rom_.data(), exdos->second.data(), std::min(exdos_rom_.size(), exdos->second.size()));
|
||||
}
|
||||
|
||||
// Seed key state.
|
||||
clear_all_keys();
|
||||
|
||||
// Take a reasonable guess at the initial memory configuration:
|
||||
// put EXOS into the first bank since this is a Z80 and therefore
|
||||
// starts from address 0; the third instruction in EXOS is a jump
|
||||
// to $c02e so it's reasonable to assume EXOS is in the highest bank
|
||||
// too, and it appears to act correctly if it's the first 16kb that's
|
||||
// in the highest bank. From there I guess: all banks are initialised
|
||||
// to 0.
|
||||
page<0>(0x00);
|
||||
page<1>(0x00);
|
||||
page<2>(0x00);
|
||||
page<3>(0x00);
|
||||
|
||||
// Pass on any media.
|
||||
insert_media(target.media);
|
||||
if(!target.loading_command.empty()) {
|
||||
type_string(target.loading_command);
|
||||
}
|
||||
|
||||
// Ensure the splash screen is automatically skipped if any media has been provided.
|
||||
if(!target.media.empty()) {
|
||||
should_skip_splash_screen_ = !target.media.empty();
|
||||
typer_delay_ = 2;
|
||||
}
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
// MARK: - Z80::BusHandler.
|
||||
forceinline void advance_nick(HalfCycles duration) {
|
||||
if(nick_ += duration) {
|
||||
const auto nick = nick_.last_valid();
|
||||
const bool nick_interrupt_line = nick->get_interrupt_line();
|
||||
if(nick_interrupt_line && !previous_nick_interrupt_line_) {
|
||||
set_interrupts(uint8_t(Dave::Interrupt::Nick), nick_.last_sequence_point_overrun());
|
||||
}
|
||||
previous_nick_interrupt_line_ = nick_interrupt_line;
|
||||
}
|
||||
}
|
||||
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
using PartialMachineCycle = CPU::Z80::PartialMachineCycle;
|
||||
const uint16_t address = cycle.address ? *cycle.address : 0x0000;
|
||||
|
||||
// Calculate an access penalty, if applicable.
|
||||
//
|
||||
// Rule applied here, which is slightly inferred:
|
||||
//
|
||||
// Non-video reads and writes are delayed by exactly a cycle or not delayed at all,
|
||||
// depending on the programmer's configuration of Dave.
|
||||
//
|
||||
// Video reads and writes, and Nick port accesses, are delayed so that the last
|
||||
// clock cycle of the machine cycle falls wholly inside the designated Z80 access
|
||||
// window, per Nick.
|
||||
//
|
||||
// The switch statement below just attempts to implement that logic.
|
||||
//
|
||||
HalfCycles penalty;
|
||||
switch(cycle.operation) {
|
||||
default: break;
|
||||
|
||||
// For non-video pauses, insert during the initial part of the bus cycle.
|
||||
case PartialMachineCycle::ReadStart:
|
||||
case PartialMachineCycle::WriteStart:
|
||||
if(!is_video_[address >> 14] && wait_mode_ == WaitMode::OnAllAccesses) {
|
||||
penalty = dave_delay_;
|
||||
}
|
||||
break;
|
||||
case PartialMachineCycle::ReadOpcodeStart: {
|
||||
if(is_video_[address >> 14]) {
|
||||
// Query Nick for the amount of delay that would occur with one cycle left
|
||||
// in this read opcode.
|
||||
const auto delay_time = nick_.time_since_flush(HalfCycles(2));
|
||||
const auto delay = nick_.last_valid()->get_time_until_z80_slot(delay_time);
|
||||
penalty = nick_.back_map(delay, delay_time);
|
||||
} else if(wait_mode_ != WaitMode::None) {
|
||||
penalty = dave_delay_;
|
||||
}
|
||||
} break;
|
||||
|
||||
// Video pauses: insert right at the end of the bus cycle.
|
||||
case PartialMachineCycle::Write:
|
||||
// Ensure all video that should have been collected prior to
|
||||
// this write has been.
|
||||
if(is_video_[address >> 14]) {
|
||||
nick_.flush();
|
||||
}
|
||||
[[fallthrough]];
|
||||
|
||||
case PartialMachineCycle::Read:
|
||||
if(is_video_[address >> 14]) {
|
||||
// Get delay, in Nick cycles, for a Z80 access that occurs in 0.5
|
||||
// cycles from now (i.e. with one cycle left to run).
|
||||
const auto delay_time = nick_.time_since_flush(HalfCycles(1));
|
||||
const auto delay = nick_.last_valid()->get_time_until_z80_slot(delay_time);
|
||||
penalty = nick_.back_map(delay, delay_time);
|
||||
}
|
||||
break;
|
||||
|
||||
case PartialMachineCycle::Input:
|
||||
case PartialMachineCycle::Output: {
|
||||
if((address & 0xf0) == 0x80) {
|
||||
// Get delay, in Nick cycles, for a Z80 access that occurs in 0.5
|
||||
// cycles from now (i.e. with one cycle left to run).
|
||||
const auto delay_time = nick_.time_since_flush(HalfCycles(1));
|
||||
const auto delay = nick_.last_valid()->get_time_until_z80_slot(delay_time);
|
||||
penalty = nick_.back_map(delay, delay_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HalfCycles full_length = cycle.length + penalty;
|
||||
time_since_audio_update_ += full_length;
|
||||
advance_nick(full_length);
|
||||
if(dave_timer_ += full_length) {
|
||||
set_interrupts(dave_timer_.last_valid()->get_new_interrupts(), dave_timer_.last_sequence_point_overrun());
|
||||
}
|
||||
|
||||
// The WD/etc runs at a nominal 8Mhz.
|
||||
if constexpr (has_disk_controller) {
|
||||
exdos_.run_for(Cycles(full_length.as_integral()));
|
||||
}
|
||||
|
||||
switch(cycle.operation) {
|
||||
default: break;
|
||||
|
||||
case PartialMachineCycle::Interrupt:
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
|
||||
case PartialMachineCycle::Input:
|
||||
switch(address & 0xff) {
|
||||
default:
|
||||
LOG("Unhandled input from " << PADHEX(2) << (address & 0xff));
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
|
||||
case 0x10: case 0x11: case 0x12: case 0x13:
|
||||
case 0x14: case 0x15: case 0x16: case 0x17:
|
||||
if constexpr (has_disk_controller) {
|
||||
*cycle.value = exdos_.read(address);
|
||||
} else {
|
||||
*cycle.value = 0xff;
|
||||
}
|
||||
break;
|
||||
case 0x18: case 0x19: case 0x1a: case 0x1b:
|
||||
case 0x1c: case 0x1d: case 0x1e: case 0x1f:
|
||||
if constexpr (has_disk_controller) {
|
||||
*cycle.value = exdos_.get_control_register();
|
||||
} else {
|
||||
*cycle.value = 0xff;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x80: case 0x81: case 0x82: case 0x83:
|
||||
case 0x84: case 0x85: case 0x86: case 0x87:
|
||||
case 0x88: case 0x89: case 0x8a: case 0x8b:
|
||||
case 0x8c: case 0x8d: case 0x8e: case 0x8f:
|
||||
*cycle.value = nick_->read();
|
||||
break;
|
||||
|
||||
case 0xb0: *cycle.value = pages_[0]; break;
|
||||
case 0xb1: *cycle.value = pages_[1]; break;
|
||||
case 0xb2: *cycle.value = pages_[2]; break;
|
||||
case 0xb3: *cycle.value = pages_[3]; break;
|
||||
|
||||
case 0xb4:
|
||||
*cycle.value =
|
||||
(nick_->get_interrupt_line() ? 0x10 : 0x00) |
|
||||
dave_timer_->get_divider_state() |
|
||||
interrupt_state_;
|
||||
break;
|
||||
case 0xb5:
|
||||
if(active_key_line_ < key_lines_.size()) {
|
||||
*cycle.value = key_lines_[active_key_line_];
|
||||
} else {
|
||||
*cycle.value = 0xff;
|
||||
}
|
||||
break;
|
||||
case 0xb6: {
|
||||
// TODO: selected keyboard row, 0 to 9, should return one bit of joystick
|
||||
// input. That being the case:
|
||||
//
|
||||
// b0: joystick input
|
||||
// b1, b2: unused (in theory read from control port, but not used by any hardware)
|
||||
// b3: 0 = printer ready; 1 = not ready
|
||||
// b4: serial, data in
|
||||
// b5: serial, status in
|
||||
// b6: tape input volume level, 0 = high, 1 = low
|
||||
// b7: tape data input
|
||||
*cycle.value = 0xff;
|
||||
} break;
|
||||
}
|
||||
break;
|
||||
|
||||
case PartialMachineCycle::Output:
|
||||
switch(address & 0xff) {
|
||||
default:
|
||||
LOG("Unhandled output: " << PADHEX(2) << *cycle.value << " to " << PADHEX(2) << (address & 0xff));
|
||||
break;
|
||||
|
||||
case 0x10: case 0x11: case 0x12: case 0x13:
|
||||
case 0x14: case 0x15: case 0x16: case 0x17:
|
||||
if constexpr(has_disk_controller) {
|
||||
exdos_.write(address, *cycle.value);
|
||||
}
|
||||
break;
|
||||
case 0x18: case 0x19: case 0x1a: case 0x1b:
|
||||
case 0x1c: case 0x1d: case 0x1e: case 0x1f:
|
||||
if constexpr(has_disk_controller) {
|
||||
exdos_.set_control_register(*cycle.value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x80: case 0x81: case 0x82: case 0x83:
|
||||
case 0x84: case 0x85: case 0x86: case 0x87:
|
||||
case 0x88: case 0x89: case 0x8a: case 0x8b:
|
||||
case 0x8c: case 0x8d: case 0x8e: case 0x8f:
|
||||
nick_->write(address, *cycle.value);
|
||||
break;
|
||||
|
||||
case 0xb0: page<0>(*cycle.value); break;
|
||||
case 0xb1: page<1>(*cycle.value); break;
|
||||
case 0xb2: page<2>(*cycle.value); break;
|
||||
case 0xb3: page<3>(*cycle.value); break;
|
||||
|
||||
case 0xbf:
|
||||
switch((*cycle.value >> 2)&3) {
|
||||
default: wait_mode_ = WaitMode::None; break;
|
||||
case 0: wait_mode_ = WaitMode::OnAllAccesses; break;
|
||||
case 1: wait_mode_ = WaitMode::OnM1; break;
|
||||
}
|
||||
|
||||
// Dave delays (i.e. those affecting memory areas not associated with Nick)
|
||||
// are one cycle in 8Mhz mode, two cycles in 12Mhz mode.
|
||||
dave_delay_ = HalfCycles(2 + (*cycle.value)&2);
|
||||
|
||||
[[fallthrough]];
|
||||
|
||||
case 0xa0: case 0xa1: case 0xa2: case 0xa3:
|
||||
case 0xa4: case 0xa5: case 0xa6: case 0xa7:
|
||||
case 0xa8: case 0xa9: case 0xaa: case 0xab:
|
||||
case 0xac: case 0xad: case 0xae: case 0xaf:
|
||||
update_audio();
|
||||
dave_audio_.write(address, *cycle.value);
|
||||
dave_timer_->write(address, *cycle.value);
|
||||
break;
|
||||
|
||||
case 0xb4:
|
||||
interrupt_mask_ = *cycle.value & 0x55;
|
||||
interrupt_state_ &= ~*cycle.value;
|
||||
update_interrupts();
|
||||
break;
|
||||
case 0xb5:
|
||||
// Logic here: the ROM scans the keyboard by checking ascending
|
||||
// lines. It also seems to provide a line of 0 when using port B5
|
||||
// for non-keyboard uses.
|
||||
//
|
||||
// So: use the rollover from line 9 back to line 0 as a trigger to
|
||||
// spot that a scan of the keyboard just finished. Which makes it
|
||||
// time to enqueue the next keypress.
|
||||
//
|
||||
// Re: should_skip_splash_screen_ and typer_delay_, assume that a
|
||||
// single keypress is necessary to get past the Enterprise splash
|
||||
// screen, then a pause in keypressing while BASIC or whatever
|
||||
// starts up, then presses can resume.
|
||||
if(active_key_line_ == 9 && !(*cycle.value & 0xf) && (should_skip_splash_screen_ || typer_)) {
|
||||
if(should_skip_splash_screen_) {
|
||||
set_key_state(uint16_t(Key::Space), typer_delay_);
|
||||
if(typer_delay_) {
|
||||
--typer_delay_;
|
||||
} else {
|
||||
typer_delay_ = 60;
|
||||
should_skip_splash_screen_ = false;
|
||||
}
|
||||
} else {
|
||||
if(!typer_delay_) {
|
||||
if(!typer_->type_next_character()) {
|
||||
clear_all_keys();
|
||||
typer_ = nullptr;
|
||||
}
|
||||
} else {
|
||||
--typer_delay_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
active_key_line_ = *cycle.value & 0xf;
|
||||
// TODO:
|
||||
//
|
||||
// b4: strobe output for printer
|
||||
// b5: tape sound control (?)
|
||||
// b6: tape motor control 1, 1 = on
|
||||
// b7: tape motor control 2, 1 = on
|
||||
break;
|
||||
case 0xb6:
|
||||
// Just 8 bits of printer data.
|
||||
LOG("TODO: printer output " << PADHEX(2) << *cycle.value);
|
||||
break;
|
||||
case 0xb7:
|
||||
// b0 = serial data out
|
||||
// b1 = serial status out
|
||||
LOG("TODO: serial output " << PADHEX(2) << *cycle.value);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case PartialMachineCycle::Read:
|
||||
case PartialMachineCycle::ReadOpcode:
|
||||
if(read_pointers_[address >> 14]) {
|
||||
*cycle.value = read_pointers_[address >> 14][address];
|
||||
} else {
|
||||
*cycle.value = 0xff;
|
||||
}
|
||||
break;
|
||||
|
||||
case PartialMachineCycle::Write:
|
||||
if(write_pointers_[address >> 14]) {
|
||||
write_pointers_[address >> 14][address] = *cycle.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return penalty;
|
||||
}
|
||||
|
||||
void flush() {
|
||||
nick_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
private:
|
||||
// MARK: - Memory layout
|
||||
std::array<uint8_t, 256 * 1024> ram_{};
|
||||
std::array<uint8_t, 64 * 1024> exos_;
|
||||
std::array<uint8_t, 16 * 1024> basic_;
|
||||
std::array<uint8_t, 16 * 1024> exdos_rom_;
|
||||
std::array<uint8_t, 32 * 1024> epdos_rom_;
|
||||
const uint8_t min_ram_slot_;
|
||||
|
||||
const uint8_t *read_pointers_[4] = {nullptr, nullptr, nullptr, nullptr};
|
||||
uint8_t *write_pointers_[4] = {nullptr, nullptr, nullptr, nullptr};
|
||||
uint8_t pages_[4] = {0x80, 0x80, 0x80, 0x80};
|
||||
|
||||
template <size_t slot> void page(uint8_t offset) {
|
||||
pages_[slot] = offset;
|
||||
|
||||
#define Map(location, source) \
|
||||
if(offset >= location && offset < location + source.size() / 0x4000) { \
|
||||
page<slot>(&source[(offset - location) * 0x4000], nullptr); \
|
||||
is_video_[slot] = false; \
|
||||
return; \
|
||||
}
|
||||
|
||||
Map(0, exos_);
|
||||
Map(16, basic_);
|
||||
Map(32, exdos_rom_);
|
||||
Map(48, epdos_rom_);
|
||||
|
||||
#undef Map
|
||||
|
||||
// Of whatever size of RAM I've declared above, use only the final portion.
|
||||
// This correlated with Nick always having been handed the final 64kb and,
|
||||
// at least while the RAM is the first thing declared above, does a little
|
||||
// to benefit data locality. Albeit not in a useful sense.
|
||||
if(offset >= min_ram_slot_) {
|
||||
const auto ram_floor = 4194304 - ram_.size();
|
||||
const size_t address = offset * 0x4000 - ram_floor;
|
||||
is_video_[slot] = offset >= 0xfc; // TODO: this hard-codes a 64kb video assumption.
|
||||
page<slot>(&ram_[address], &ram_[address]);
|
||||
return;
|
||||
}
|
||||
|
||||
page<slot>(nullptr, nullptr);
|
||||
}
|
||||
|
||||
template <size_t slot> void page(const uint8_t *read, uint8_t *write) {
|
||||
read_pointers_[slot] = read ? read - (slot * 0x4000) : nullptr;
|
||||
write_pointers_[slot] = write ? write - (slot * 0x4000) : nullptr;
|
||||
}
|
||||
|
||||
// MARK: - Memory Timing
|
||||
|
||||
// The wait mode affects all memory accesses _outside of the video area_.
|
||||
enum class WaitMode {
|
||||
None,
|
||||
OnM1,
|
||||
OnAllAccesses
|
||||
} wait_mode_ = WaitMode::OnAllAccesses;
|
||||
bool is_video_[4]{};
|
||||
|
||||
// MARK: - ScanProducer
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
nick_.last_valid()->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const override {
|
||||
return nick_.last_valid()->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
nick_.last_valid()->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType get_display_type() const final {
|
||||
return nick_.last_valid()->get_display_type();
|
||||
}
|
||||
|
||||
// MARK: - AudioProducer
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
// MARK: - TimedMachine
|
||||
void run_for(const Cycles cycles) override {
|
||||
z80_.run_for(cycles);
|
||||
}
|
||||
|
||||
// MARK: - KeyboardMachine
|
||||
Enterprise::KeyboardMapper keyboard_mapper_;
|
||||
KeyboardMapper *get_keyboard_mapper() final {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
|
||||
uint8_t active_key_line_ = 0;
|
||||
std::array<uint8_t, 10> key_lines_;
|
||||
void set_key_state(uint16_t key, bool is_pressed) final {
|
||||
if(is_pressed) {
|
||||
key_lines_[key >> 8] &= ~uint8_t(key);
|
||||
} else {
|
||||
key_lines_[key >> 8] |= uint8_t(key);
|
||||
}
|
||||
}
|
||||
|
||||
void clear_all_keys() final {
|
||||
key_lines_.fill(0xff);
|
||||
}
|
||||
|
||||
// MARK: - Utility::TypeRecipient
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient<CharacterMapper>::add_typer(string);
|
||||
|
||||
if(z80_.get_is_resetting()) {
|
||||
should_skip_splash_screen_ = true;
|
||||
typer_delay_ = 1;
|
||||
} else {
|
||||
should_skip_splash_screen_ = false;
|
||||
typer_delay_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool can_type(char c) const final {
|
||||
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
|
||||
}
|
||||
|
||||
bool should_skip_splash_screen_ = false;
|
||||
int typer_delay_ = 30;
|
||||
|
||||
// MARK: - MediaTarget
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
if constexpr (has_disk_controller) {
|
||||
if(!media.disks.empty()) {
|
||||
exdos_.set_disk(media.disks.front(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: - Interrupts
|
||||
|
||||
uint8_t interrupt_mask_ = 0x00, interrupt_state_ = 0x00;
|
||||
void set_interrupts(uint8_t mask, HalfCycles offset = HalfCycles(0)) {
|
||||
interrupt_state_ |= uint8_t(mask);
|
||||
update_interrupts(offset);
|
||||
}
|
||||
void update_interrupts(HalfCycles offset = HalfCycles(0)) {
|
||||
z80_.set_interrupt_line((interrupt_state_ >> 1) & interrupt_mask_, offset);
|
||||
}
|
||||
|
||||
// MARK: - Chips.
|
||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||
NickType nick_;
|
||||
bool previous_nick_interrupt_line_ = false;
|
||||
// Cf. timing guesses above.
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
Dave::Audio dave_audio_;
|
||||
Outputs::Speaker::LowpassSpeaker<Dave::Audio> speaker_;
|
||||
HalfCycles time_since_audio_update_;
|
||||
|
||||
HalfCycles dave_delay_ = HalfCycles(2);
|
||||
|
||||
// The divider supplied to the JustInTimeActor and the manual divider used in
|
||||
// update_audio() should match.
|
||||
static constexpr int dave_divider = 8;
|
||||
JustInTimeActor<Dave::TimedInterruptSource, HalfCycles, 1, dave_divider> dave_timer_;
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, time_since_audio_update_.divide_cycles(Cycles(dave_divider)));
|
||||
}
|
||||
|
||||
// MARK: - EXDos card.
|
||||
EXDos exdos_;
|
||||
|
||||
// MARK: - Activity Source
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
if constexpr (has_disk_controller) {
|
||||
exdos_.set_activity_observer(observer);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::unique_ptr<Reflection::Struct> get_options() final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->output = get_video_signal_configurable();
|
||||
return options;
|
||||
}
|
||||
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
set_video_signal_configurable(options->output);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
using namespace Enterprise;
|
||||
|
||||
Machine *Machine::Enterprise(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
using Target = Analyser::Static::Enterprise::Target;
|
||||
const Target *const enterprise_target = dynamic_cast<const Target *>(target);
|
||||
|
||||
#define BuildMachine(exdos, sixmhz) \
|
||||
if((enterprise_target->dos == Target::DOS::None) != exdos && (enterprise_target->speed == Target::Speed::SixMHz) == sixmhz) { \
|
||||
return new Enterprise::ConcreteMachine<exdos, sixmhz>(*enterprise_target, rom_fetcher); \
|
||||
}
|
||||
|
||||
BuildMachine(false, false);
|
||||
BuildMachine(false, true);
|
||||
BuildMachine(true, false);
|
||||
BuildMachine(true, true);
|
||||
|
||||
#undef BuildMachine
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
47
Machines/Enterprise/Enterprise.hpp
Normal file
47
Machines/Enterprise/Enterprise.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Enterprise.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 10/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Enterprise_hpp
|
||||
#define Enterprise_hpp
|
||||
|
||||
#include "../../Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "../../Configurable/Configurable.hpp"
|
||||
#include "../../Configurable/StandardOptions.hpp"
|
||||
#include "../ROMMachine.hpp"
|
||||
|
||||
namespace Enterprise {
|
||||
|
||||
/*!
|
||||
@abstract Represents an Elan Enterprise.
|
||||
|
||||
@discussion An instance of Enterprise::Machine represents the current state of an
|
||||
Elan Enterprise.
|
||||
*/
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
|
||||
static Machine *Enterprise(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
|
||||
|
||||
/// Defines the runtime options available for an Enterprise.
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ? Configurable::Display::RGB : Configurable::Display::CompositeColour) {
|
||||
if(needs_declare()) {
|
||||
declare_display_option();
|
||||
limit_enum(&output, Configurable::Display::RGB, Configurable::Display::CompositeColour, Configurable::Display::CompositeMonochrome, -1);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
#endif /* Enterprise_hpp */
|
||||
151
Machines/Enterprise/Keyboard.cpp
Normal file
151
Machines/Enterprise/Keyboard.cpp
Normal file
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// Keyboard.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 17/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Keyboard.hpp"
|
||||
|
||||
using namespace Enterprise;
|
||||
|
||||
uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const {
|
||||
#define BIND(source, dest) case Inputs::Keyboard::Key::source: return uint16_t(Key::dest)
|
||||
switch(key) {
|
||||
default: break;
|
||||
|
||||
BIND(Backslash, Backslash);
|
||||
BIND(CapsLock, Lock);
|
||||
BIND(Tab, Tab);
|
||||
BIND(Escape, Escape);
|
||||
BIND(Hyphen, Hyphen);
|
||||
BIND(Equals, Caret);
|
||||
BIND(Backspace, Erase);
|
||||
BIND(Delete, Delete);
|
||||
BIND(Semicolon, Semicolon);
|
||||
BIND(Quote, Colon);
|
||||
BIND(OpenSquareBracket, OpenSquareBracket);
|
||||
BIND(CloseSquareBracket, CloseSquareBracket);
|
||||
|
||||
BIND(End, Stop);
|
||||
BIND(Insert, Insert);
|
||||
BIND(BackTick, At);
|
||||
|
||||
BIND(k1, k1); BIND(k2, k2); BIND(k3, k3); BIND(k4, k4); BIND(k5, k5);
|
||||
BIND(k6, k6); BIND(k7, k7); BIND(k8, k8); BIND(k9, k9); BIND(k0, k0);
|
||||
|
||||
BIND(F1, F1); BIND(F2, F2); BIND(F3, F3); BIND(F4, F4);
|
||||
BIND(F5, F5); BIND(F6, F6); BIND(F7, F7); BIND(F8, F8);
|
||||
|
||||
BIND(Keypad1, F1); BIND(Keypad2, F2); BIND(Keypad3, F3); BIND(Keypad4, F4);
|
||||
BIND(Keypad5, F5); BIND(Keypad6, F6); BIND(Keypad7, F7); BIND(Keypad8, F8);
|
||||
|
||||
BIND(Q, Q); BIND(W, W); BIND(E, E); BIND(R, R); BIND(T, T);
|
||||
BIND(Y, Y); BIND(U, U); BIND(I, I); BIND(O, O); BIND(P, P);
|
||||
|
||||
BIND(A, A); BIND(S, S); BIND(D, D); BIND(F, F); BIND(G, G);
|
||||
BIND(H, H); BIND(J, J); BIND(K, K); BIND(L, L);
|
||||
|
||||
BIND(Z, Z); BIND(X, X); BIND(C, C); BIND(V, V);
|
||||
BIND(B, B); BIND(N, N); BIND(M, M);
|
||||
|
||||
BIND(FullStop, FullStop);
|
||||
BIND(Comma, Comma);
|
||||
BIND(ForwardSlash, ForwardSlash);
|
||||
|
||||
BIND(Space, Space); BIND(Enter, Enter);
|
||||
|
||||
BIND(LeftShift, LeftShift);
|
||||
BIND(RightShift, RightShift);
|
||||
BIND(LeftOption, Alt);
|
||||
BIND(RightOption, Alt);
|
||||
BIND(LeftControl, Control);
|
||||
BIND(RightControl, Control);
|
||||
|
||||
BIND(Left, Left);
|
||||
BIND(Right, Right);
|
||||
BIND(Up, Up);
|
||||
BIND(Down, Down);
|
||||
}
|
||||
#undef BIND
|
||||
|
||||
return MachineTypes::MappedKeyboardMachine::KeyNotMapped;
|
||||
}
|
||||
|
||||
const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
#define KEYS(x) {uint16_t(x), MachineTypes::MappedKeyboardMachine::KeyEndSequence}
|
||||
#define SHIFT(x) {uint16_t(Key::LeftShift), uint16_t(x), MachineTypes::MappedKeyboardMachine::KeyEndSequence}
|
||||
#define _ {MachineTypes::MappedKeyboardMachine::KeyNotMapped}
|
||||
static KeySequence key_sequences[] = {
|
||||
/* NUL */ _, /* SOH */ _,
|
||||
/* STX */ _, /* ETX */ _,
|
||||
/* EOT */ _, /* ENQ */ _,
|
||||
/* ACK */ _, /* BEL */ _,
|
||||
/* BS */ KEYS(Key::Erase), /* HT */ KEYS(Key::Tab),
|
||||
/* LF */ KEYS(Key::Enter), /* VT */ _,
|
||||
/* FF */ _, /* CR */ KEYS(Key::Enter),
|
||||
/* SO */ _, /* SI */ _,
|
||||
/* DLE */ _, /* DC1 */ _,
|
||||
/* DC2 */ _, /* DC3 */ _,
|
||||
/* DC4 */ _, /* NAK */ _,
|
||||
/* SYN */ _, /* ETB */ _,
|
||||
/* CAN */ _, /* EM */ _,
|
||||
/* SUB */ _, /* ESC */ KEYS(Key::Escape),
|
||||
/* FS */ _, /* GS */ _,
|
||||
/* RS */ _, /* US */ _,
|
||||
/* space */ KEYS(Key::Space), /* ! */ SHIFT(Key::k1),
|
||||
/* " */ SHIFT(Key::k2), /* # */ _,
|
||||
/* $ */ SHIFT(Key::k4), /* % */ SHIFT(Key::k5),
|
||||
/* & */ SHIFT(Key::k6), /* ' */ SHIFT(Key::k7),
|
||||
/* ( */ SHIFT(Key::k8), /* ) */ SHIFT(Key::k9),
|
||||
/* * */ SHIFT(Key::Colon), /* + */ SHIFT(Key::Semicolon),
|
||||
/* , */ KEYS(Key::Comma), /* - */ KEYS(Key::Hyphen),
|
||||
/* . */ KEYS(Key::FullStop), /* / */ KEYS(Key::ForwardSlash),
|
||||
/* 0 */ KEYS(Key::k0), /* 1 */ KEYS(Key::k1),
|
||||
/* 2 */ KEYS(Key::k2), /* 3 */ KEYS(Key::k3),
|
||||
/* 4 */ KEYS(Key::k4), /* 5 */ KEYS(Key::k5),
|
||||
/* 6 */ KEYS(Key::k6), /* 7 */ KEYS(Key::k7),
|
||||
/* 8 */ KEYS(Key::k8), /* 9 */ KEYS(Key::k9),
|
||||
/* : */ KEYS(Key::Colon), /* ; */ KEYS(Key::Semicolon),
|
||||
/* < */ SHIFT(Key::Comma), /* = */ SHIFT(Key::Hyphen),
|
||||
/* > */ SHIFT(Key::FullStop), /* ? */ SHIFT(Key::ForwardSlash),
|
||||
/* @ */ KEYS(Key::At), /* A */ SHIFT(Key::A),
|
||||
/* B */ SHIFT(Key::B), /* C */ SHIFT(Key::C),
|
||||
/* D */ SHIFT(Key::D), /* E */ SHIFT(Key::E),
|
||||
/* F */ SHIFT(Key::F), /* G */ SHIFT(Key::G),
|
||||
/* H */ SHIFT(Key::H), /* I */ SHIFT(Key::I),
|
||||
/* J */ SHIFT(Key::J), /* K */ SHIFT(Key::K),
|
||||
/* L */ SHIFT(Key::L), /* M */ SHIFT(Key::M),
|
||||
/* N */ SHIFT(Key::N), /* O */ SHIFT(Key::O),
|
||||
/* P */ SHIFT(Key::P), /* Q */ SHIFT(Key::Q),
|
||||
/* R */ SHIFT(Key::R), /* S */ SHIFT(Key::S),
|
||||
/* T */ SHIFT(Key::T), /* U */ SHIFT(Key::U),
|
||||
/* V */ SHIFT(Key::V), /* W */ SHIFT(Key::W),
|
||||
/* X */ SHIFT(Key::X), /* Y */ SHIFT(Key::Y),
|
||||
/* Z */ SHIFT(Key::Z), /* [ */ KEYS(Key::OpenSquareBracket),
|
||||
/* \ */ KEYS(Key::Backslash), /* ] */ KEYS(Key::CloseSquareBracket),
|
||||
/* ^ */ SHIFT(Key::Caret), /* _ */ SHIFT(Key::k0),
|
||||
/* ` */ SHIFT(Key::At), /* a */ KEYS(Key::A),
|
||||
/* b */ KEYS(Key::B), /* c */ KEYS(Key::C),
|
||||
/* d */ KEYS(Key::D), /* e */ KEYS(Key::E),
|
||||
/* f */ KEYS(Key::F), /* g */ KEYS(Key::G),
|
||||
/* h */ KEYS(Key::H), /* i */ KEYS(Key::I),
|
||||
/* j */ KEYS(Key::J), /* k */ KEYS(Key::K),
|
||||
/* l */ KEYS(Key::L), /* m */ KEYS(Key::M),
|
||||
/* n */ KEYS(Key::N), /* o */ KEYS(Key::O),
|
||||
/* p */ KEYS(Key::P), /* q */ KEYS(Key::Q),
|
||||
/* r */ KEYS(Key::R), /* s */ KEYS(Key::S),
|
||||
/* t */ KEYS(Key::T), /* u */ KEYS(Key::U),
|
||||
/* v */ KEYS(Key::V), /* w */ KEYS(Key::W),
|
||||
/* x */ KEYS(Key::X), /* y */ KEYS(Key::Y),
|
||||
/* z */ KEYS(Key::Z), /* { */ SHIFT(Key::OpenSquareBracket),
|
||||
/* | */ SHIFT(Key::Backslash), /* } */ SHIFT(Key::CloseSquareBracket),
|
||||
/* ~ */ SHIFT(Key::Caret)
|
||||
};
|
||||
#undef _
|
||||
#undef SHIFT
|
||||
#undef KEYS
|
||||
|
||||
return table_lookup_sequence_for_character(key_sequences, character);
|
||||
}
|
||||
66
Machines/Enterprise/Keyboard.hpp
Normal file
66
Machines/Enterprise/Keyboard.hpp
Normal file
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// Keyboard.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 17/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Machines_Enterprise_Keyboard_hpp
|
||||
#define Machines_Enterprise_Keyboard_hpp
|
||||
|
||||
#include "../KeyboardMachine.hpp"
|
||||
#include "../Utility/Typer.hpp"
|
||||
|
||||
namespace Enterprise {
|
||||
|
||||
#define KeyCode(line, mask) (line << 8) | mask
|
||||
|
||||
enum class Key: uint16_t {
|
||||
N = 0x0000 | 0x01, Backslash = 0x0000 | 0x02, B = 0x0000 | 0x04, C = 0x0000 | 0x08,
|
||||
V = 0x0000 | 0x10, X = 0x0000 | 0x20, Z = 0x0000 | 0x40, LeftShift = 0x0000 | 0x80,
|
||||
|
||||
H = 0x0100 | 0x01, Lock = 0x0100 | 0x02, G = 0x0100 | 0x04, D = 0x0100 | 0x08,
|
||||
F = 0x0100 | 0x10, S = 0x0100 | 0x20, A = 0x0100 | 0x40, Control = 0x0100 | 0x80,
|
||||
|
||||
U = 0x0200 | 0x01, Q = 0x0200 | 0x02, Y = 0x0200 | 0x04, R = 0x0200 | 0x08,
|
||||
T = 0x0200 | 0x10, E = 0x0200 | 0x20, W = 0x0200 | 0x40, Tab = 0x0200 | 0x80,
|
||||
|
||||
k7 = 0x0300 | 0x01, k1 = 0x0300 | 0x02, k6 = 0x0300 | 0x04, k4 = 0x0300 | 0x08,
|
||||
k5 = 0x0300 | 0x10, k3 = 0x0300 | 0x20, k2 = 0x0300 | 0x40, Escape = 0x0300 | 0x80,
|
||||
|
||||
F4 = 0x0400 | 0x01, F8 = 0x0400 | 0x02, F3 = 0x0400 | 0x04, F6 = 0x0400 | 0x08,
|
||||
F5 = 0x0400 | 0x10, F7 = 0x0400 | 0x20, F2 = 0x0400 | 0x40, F1 = 0x0400 | 0x80,
|
||||
|
||||
k8 = 0x0500 | 0x01, k9 = 0x0500 | 0x04, Hyphen = 0x0500 | 0x08,
|
||||
k0 = 0x0500 | 0x10, Caret = 0x0500 | 0x20, Erase = 0x0500 | 0x40,
|
||||
|
||||
J = 0x0600 | 0x01, K = 0x0600 | 0x04, Semicolon = 0x0600 | 0x08,
|
||||
L = 0x0600 | 0x10, Colon = 0x0600 | 0x20, CloseSquareBracket = 0x0600 | 0x40,
|
||||
|
||||
Stop = 0x0700 | 0x01, Down = 0x0700 | 0x02, Right = 0x0700 | 0x04, Up = 0x0700 | 0x08,
|
||||
Hold = 0x0700 | 0x10, Left = 0x0700 | 0x20, Enter = 0x0700 | 0x40, Alt = 0x0700 | 0x80,
|
||||
|
||||
M = 0x0800 | 0x01, Delete = 0x0800 | 0x02, Comma = 0x0800 | 0x04,
|
||||
ForwardSlash = 0x0800 | 0x08,
|
||||
FullStop = 0x0800 | 0x10,
|
||||
RightShift = 0x0800 | 0x20, Space = 0x0800 | 0x40, Insert = 0x0800 | 0x80,
|
||||
|
||||
I = 0x0900 | 0x01, O = 0x0900 | 0x04, At = 0x0900 | 0x08,
|
||||
P = 0x0900 | 0x10,
|
||||
OpenSquareBracket = 0x0900 | 0x20
|
||||
};
|
||||
|
||||
#undef KeyCode
|
||||
|
||||
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const final;
|
||||
};
|
||||
|
||||
struct CharacterMapper: public ::Utility::CharacterMapper {
|
||||
const uint16_t *sequence_for_character(char character) const override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* Keyboard_hpp */
|
||||
629
Machines/Enterprise/Nick.cpp
Normal file
629
Machines/Enterprise/Nick.cpp
Normal file
@@ -0,0 +1,629 @@
|
||||
//
|
||||
// Nick.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Nick.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
uint16_t mapped_colour(uint8_t source) {
|
||||
// On the Enterprise, red and green are 3-bit quantities; blue is a 2-bit quantity.
|
||||
int red = ((source&0x01) << 2) | ((source&0x08) >> 2) | ((source&0x40) >> 6);
|
||||
int green = ((source&0x02) << 1) | ((source&0x10) >> 3) | ((source&0x80) >> 7);
|
||||
int blue = ((source&0x04) >> 1) | ((source&0x20) >> 5);
|
||||
|
||||
assert(red <= 7);
|
||||
assert(green <= 7);
|
||||
assert(blue <= 3);
|
||||
|
||||
red = (red << 1) + (red >> 3);
|
||||
green = (green << 1) + (green >> 3);
|
||||
blue = (blue << 2) + blue;
|
||||
|
||||
assert(red <= 15);
|
||||
assert(green <= 15);
|
||||
assert(blue <= 15);
|
||||
|
||||
// Duplicate bits where necessary to map to a full 4-bit range per channel.
|
||||
const uint8_t parts[2] = {
|
||||
uint8_t(
|
||||
red
|
||||
),
|
||||
uint8_t(
|
||||
(green << 4) + blue
|
||||
)
|
||||
};
|
||||
return *reinterpret_cast<const uint16_t *>(parts);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
using namespace Enterprise;
|
||||
|
||||
Nick::Nick(const uint8_t *ram) :
|
||||
crt_(57*16, 16, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red4Green4Blue4),
|
||||
ram_(ram) {
|
||||
|
||||
// Just use RGB for now.
|
||||
set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
|
||||
// Crop to the centre 90% of the display.
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f));
|
||||
}
|
||||
|
||||
void Nick::write(uint16_t address, uint8_t value) {
|
||||
switch(address & 3) {
|
||||
case 0:
|
||||
// Ignored: everything to do with external colour.
|
||||
for(int c = 0; c < 8; c++) {
|
||||
palette_[c + 8] = mapped_colour(uint8_t(((value & 0x1f) << 3) + c));
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if(output_type_ == OutputType::Border) {
|
||||
set_output_type(OutputType::Border, true);
|
||||
}
|
||||
border_colour_ = mapped_colour(value);
|
||||
break;
|
||||
case 2:
|
||||
line_parameter_base_ = uint16_t((line_parameter_base_ & 0xf000) | (value << 4));
|
||||
break;
|
||||
case 3:
|
||||
line_parameter_base_ = uint16_t((line_parameter_base_ & 0x0ff0) | (value << 12));
|
||||
|
||||
// Still a mystery to me: the exact meaning of the top two bits here. For now
|
||||
// just treat a 0 -> 1 transition of the MSB as a forced frame restart.
|
||||
if((value^line_parameter_control_) & value & 0x80) {
|
||||
// For now: just force this to be the final line of this mode block.
|
||||
// I'm unclear whether I should also reset the horizontal counter
|
||||
// (i.e. completely abandon current video phase).
|
||||
lines_remaining_ = 0xff;
|
||||
should_reload_line_parameters_ = true;
|
||||
}
|
||||
line_parameter_control_ = value & 0xc0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t Nick::read() {
|
||||
return last_read_;
|
||||
}
|
||||
|
||||
Cycles Nick::get_time_until_z80_slot(Cycles after_period) const {
|
||||
// Place Z80 accesses in the first six cycles in each sixteen-cycle window.
|
||||
// That models video accesses as being the final ten. Which has the net effect
|
||||
// of responding to the line parameter table interrupt flag as soon as it's
|
||||
// loaded.
|
||||
|
||||
// Assumed below: the Z80 can start its final cycle anywhere in the first three
|
||||
// of the permitted six.
|
||||
const int offset = (horizontal_counter_ + after_period.as<int>()) & 15;
|
||||
if(offset < 3) {
|
||||
return 0;
|
||||
} else {
|
||||
return 16 - offset;
|
||||
}
|
||||
}
|
||||
|
||||
void Nick::run_for(Cycles duration) {
|
||||
constexpr int line_length = 912;
|
||||
|
||||
#define add_window(x) \
|
||||
line_data_pointer_[0] += is_sync_or_pixels_ * line_data_per_column_increments_[0] * (x); \
|
||||
line_data_pointer_[1] += is_sync_or_pixels_ * line_data_per_column_increments_[1] * (x); \
|
||||
window += x; \
|
||||
if(window != 57 && window == left_margin_) is_sync_or_pixels_ = true; \
|
||||
if(window != 57 && window == right_margin_) is_sync_or_pixels_ = false;
|
||||
|
||||
int clocks_remaining = duration.as<int>();
|
||||
while(clocks_remaining) {
|
||||
// Determine how many cycles are left this line.
|
||||
const int clocks_this_line = std::min(clocks_remaining, line_length - horizontal_counter_);
|
||||
|
||||
// Convert that into a [start/current] and end window.
|
||||
int window = horizontal_counter_ >> 4;
|
||||
const int end_window = (horizontal_counter_ + clocks_this_line) >> 4;
|
||||
|
||||
// Advance the line counters.
|
||||
clocks_remaining -= clocks_this_line;
|
||||
horizontal_counter_ = (horizontal_counter_ + clocks_this_line) % line_length;
|
||||
|
||||
// Do nothing if a window boundary isn't crossed.
|
||||
if(window == end_window) continue;
|
||||
|
||||
// HSYNC is signalled for four windows at the start of the line.
|
||||
// I currently believe this happens regardless of Vsync mode.
|
||||
if(!window) {
|
||||
set_output_type(OutputType::Sync);
|
||||
|
||||
// There's no increment to get to 0, it happens when the horizontal_counter_
|
||||
// is reset. So test for active bit effect manually.
|
||||
if(!left_margin_) is_sync_or_pixels_ = true;
|
||||
if(!right_margin_) is_sync_or_pixels_ = false;
|
||||
}
|
||||
|
||||
// Default to noting read.
|
||||
last_read_ = 0xff;
|
||||
|
||||
while(window < 4 && window < end_window) {
|
||||
if(should_reload_line_parameters_) {
|
||||
switch(window) {
|
||||
// First slot: line count, mode and interrupt flag.
|
||||
case 0:
|
||||
// Byte 0: lines remaining.
|
||||
lines_remaining_ = ram_[line_parameter_pointer_];
|
||||
|
||||
// Byte 1: current interrupt output plus graphics modes...
|
||||
last_read_ = ram_[line_parameter_pointer_ + 1];
|
||||
|
||||
// Set the new interrupt line output.
|
||||
interrupt_line_ = ram_[line_parameter_pointer_ + 1] & 0x80;
|
||||
|
||||
// Determine the mode and depth, and hence the column size.
|
||||
mode_ = Mode((ram_[line_parameter_pointer_ + 1] >> 1)&7);
|
||||
bpp_ = 1 << ((ram_[line_parameter_pointer_ + 1] >> 5)&3);
|
||||
switch(mode_) {
|
||||
default:
|
||||
case Mode::Pixel:
|
||||
column_size_ = 16 / bpp_;
|
||||
line_data_per_column_increments_[0] = 2;
|
||||
line_data_per_column_increments_[1] = 0;
|
||||
break;
|
||||
|
||||
case Mode::LPixel:
|
||||
case Mode::CH64:
|
||||
case Mode::CH128:
|
||||
case Mode::CH256:
|
||||
column_size_ = 8 / bpp_;
|
||||
line_data_per_column_increments_[0] = 1;
|
||||
line_data_per_column_increments_[1] = 0;
|
||||
break;
|
||||
|
||||
case Mode::Attr:
|
||||
column_size_ = 8;
|
||||
line_data_per_column_increments_[0] = 1;
|
||||
line_data_per_column_increments_[1] = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
vres_ = ram_[line_parameter_pointer_ + 1] & 0x10;
|
||||
reload_line_parameter_pointer_ = ram_[line_parameter_pointer_ + 1] & 0x01;
|
||||
break;
|
||||
|
||||
// Second slot: margins and ALT/IND bits.
|
||||
case 1:
|
||||
// Determine the margins.
|
||||
left_margin_ = ram_[line_parameter_pointer_ + 2] & 0x3f;
|
||||
right_margin_ = ram_[line_parameter_pointer_ + 3] & 0x3f;
|
||||
last_read_ = ram_[line_parameter_pointer_ + 3];
|
||||
|
||||
// Set up the alternative palettes,
|
||||
switch(mode_) {
|
||||
default:
|
||||
break;
|
||||
|
||||
// NB: LSBALT/MSBALT and ALTIND0/ALTIND1 appear to have opposite effects on palette selection.
|
||||
|
||||
case Mode::Pixel:
|
||||
case Mode::LPixel: {
|
||||
const uint8_t flags = ram_[line_parameter_pointer_ + 2];
|
||||
|
||||
// Use MSBALT and LSBALT to pick the alt_ind_palettes.
|
||||
//
|
||||
// LSBALT = b6 of params[2], if set => character codes with bit 6 set should use palette indices 4... instead of 0... .
|
||||
// MSBALT = b7 of params[2], if set => character codes with bit 7 set should use palette indices 2 and 3.
|
||||
two_colour_mask_ = 0xff &~ (((flags&0x80) >> 7) | ((flags&0x40) << 1));
|
||||
|
||||
alt_ind_palettes[0] = palette_;
|
||||
alt_ind_palettes[2] = alt_ind_palettes[0] + ((flags & 0x80) ? 2 : 0);
|
||||
|
||||
alt_ind_palettes[1] = alt_ind_palettes[0] + ((flags & 0x40) ? 4 : 0);
|
||||
alt_ind_palettes[3] = alt_ind_palettes[2] + ((flags & 0x40) ? 4 : 0);
|
||||
} break;
|
||||
|
||||
case Mode::CH64:
|
||||
case Mode::CH128:
|
||||
case Mode::CH256: {
|
||||
const uint8_t flags = ram_[line_parameter_pointer_ + 3];
|
||||
|
||||
// Use ALTIND0 and ALTIND1 to pick the alt_ind_palettes.
|
||||
//
|
||||
// ALTIND1 = b6 of params[3], if set => character codes with bit 7 set should use palette indices 2 and 3.
|
||||
// ALTIND0 = b7 of params[3], if set => character codes with bit 6 set should use palette indices 4... instead of 0... .
|
||||
alt_ind_palettes[0] = palette_;
|
||||
alt_ind_palettes[2] = alt_ind_palettes[0] + ((flags & 0x40) ? 2 : 0);
|
||||
|
||||
alt_ind_palettes[1] = alt_ind_palettes[0] + ((flags & 0x80) ? 4 : 0);
|
||||
alt_ind_palettes[3] = alt_ind_palettes[2] + ((flags & 0x80) ? 4 : 0);
|
||||
} break;
|
||||
}
|
||||
break;
|
||||
|
||||
// Third slot: Line data pointer 1.
|
||||
case 2:
|
||||
start_line_data_pointer_[0] = ram_[line_parameter_pointer_ + 4];
|
||||
start_line_data_pointer_[0] |= ram_[line_parameter_pointer_ + 5] << 8;
|
||||
|
||||
line_data_pointer_[0] = start_line_data_pointer_[0];
|
||||
last_read_ = ram_[line_parameter_pointer_ + 5];
|
||||
break;
|
||||
|
||||
// Fourth slot: Line data pointer 2.
|
||||
case 3:
|
||||
start_line_data_pointer_[1] = ram_[line_parameter_pointer_ + 6];
|
||||
start_line_data_pointer_[1] |= ram_[line_parameter_pointer_ + 7] << 8;
|
||||
|
||||
line_data_pointer_[1] = start_line_data_pointer_[1];
|
||||
last_read_ = ram_[line_parameter_pointer_ + 7];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
++output_duration_;
|
||||
add_window(1);
|
||||
}
|
||||
if(window == 4) {
|
||||
if(mode_ == Mode::Vsync) {
|
||||
set_output_type(is_sync_or_pixels_ ? OutputType::Sync : OutputType::Blank);
|
||||
} else {
|
||||
set_output_type(OutputType::Blank);
|
||||
}
|
||||
}
|
||||
|
||||
// Deal with vsync mode out here.
|
||||
if(mode_ == Mode::Vsync) {
|
||||
if(window >= 4) {
|
||||
while(window < end_window) {
|
||||
// Skip straight to the next event.
|
||||
int next_event = end_window;
|
||||
if(window < left_margin_) next_event = std::min(next_event, left_margin_);
|
||||
if(window < right_margin_) next_event = std::min(next_event, right_margin_);
|
||||
|
||||
output_duration_ += next_event - window;
|
||||
add_window(next_event - window);
|
||||
set_output_type(is_sync_or_pixels_ ? OutputType::Sync : OutputType::Blank);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If present then the colour burst is output for the period from
|
||||
// the start of window 6 to the end of window 10.
|
||||
//
|
||||
// The first 8 palette entries also need to be fetched here.
|
||||
while(window < first_pixel_window_ && window < end_window) {
|
||||
if(window == 6) {
|
||||
set_output_type(OutputType::ColourBurst);
|
||||
}
|
||||
|
||||
if(should_reload_line_parameters_ && window < 8) {
|
||||
const int base = (window - 4) << 1;
|
||||
assert(base < 7);
|
||||
palette_[base] = mapped_colour(ram_[line_parameter_pointer_ + base + 8]);
|
||||
palette_[base + 1] = mapped_colour(ram_[line_parameter_pointer_ + base + 9]);
|
||||
last_read_ = ram_[line_parameter_pointer_ + base + 9];
|
||||
}
|
||||
|
||||
++output_duration_;
|
||||
add_window(1);
|
||||
}
|
||||
|
||||
if(window >= first_pixel_window_) {
|
||||
if(window == first_pixel_window_) {
|
||||
set_output_type(is_sync_or_pixels_ ? OutputType::Pixels : OutputType::Border);
|
||||
}
|
||||
|
||||
while(window < end_window) {
|
||||
int next_event = end_window;
|
||||
if(window < left_margin_) next_event = std::min(next_event, left_margin_);
|
||||
if(window < right_margin_) next_event = std::min(next_event, right_margin_);
|
||||
|
||||
if(is_sync_or_pixels_) {
|
||||
|
||||
#define DispatchBpp(func) \
|
||||
switch(bpp_) { \
|
||||
default: \
|
||||
case 1: func(1)(pixel_pointer_, output_duration); break; \
|
||||
case 2: func(2)(pixel_pointer_, output_duration); break; \
|
||||
case 4: func(4)(pixel_pointer_, output_duration); break; \
|
||||
case 8: func(8)(pixel_pointer_, output_duration); break; \
|
||||
}
|
||||
|
||||
#define pixel(x) output_pixel<x, false>
|
||||
#define lpixel(x) output_pixel<x, true>
|
||||
#define ch256(x) output_character<x, 8>
|
||||
#define ch128(x) output_character<x, 7>
|
||||
#define ch64(x) output_character<x, 6>
|
||||
#define attr(x) output_attributed<x>
|
||||
|
||||
int columns_remaining = next_event - window;
|
||||
while(columns_remaining) {
|
||||
if(!pixel_pointer_) {
|
||||
if(output_duration_) {
|
||||
set_output_type(OutputType::Pixels, true);
|
||||
}
|
||||
pixel_pointer_ = allocated_pointer_ = reinterpret_cast<uint16_t *>(crt_.begin_data(allocation_size));
|
||||
}
|
||||
|
||||
if(allocated_pointer_) {
|
||||
const int output_duration = std::min(columns_remaining, int(allocated_pointer_ + allocation_size - pixel_pointer_) / column_size_);
|
||||
|
||||
switch(mode_) {
|
||||
default:
|
||||
case Mode::Pixel: DispatchBpp(pixel); break;
|
||||
case Mode::LPixel: DispatchBpp(lpixel); break;
|
||||
case Mode::CH256: DispatchBpp(ch256); break;
|
||||
case Mode::CH128: DispatchBpp(ch128); break;
|
||||
case Mode::CH64: DispatchBpp(ch64); break;
|
||||
case Mode::Attr: DispatchBpp(attr); break;
|
||||
}
|
||||
|
||||
pixel_pointer_ += output_duration * column_size_;
|
||||
output_duration_ += output_duration;
|
||||
if(pixel_pointer_ - allocated_pointer_ == allocation_size) {
|
||||
set_output_type(OutputType::Pixels, true);
|
||||
}
|
||||
columns_remaining -= output_duration;
|
||||
add_window(output_duration);
|
||||
} else {
|
||||
output_duration_ += columns_remaining;
|
||||
add_window(columns_remaining);
|
||||
columns_remaining = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#undef attr
|
||||
#undef ch64
|
||||
#undef ch128
|
||||
#undef ch256
|
||||
#undef pixel
|
||||
#undef lpixel
|
||||
#undef DispatchBpp
|
||||
} else {
|
||||
output_duration_ += next_event - window;
|
||||
add_window(next_event - window);
|
||||
}
|
||||
|
||||
set_output_type(is_sync_or_pixels_ ? OutputType::Pixels : OutputType::Border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for end of line.
|
||||
if(!horizontal_counter_) {
|
||||
assert(window == 57);
|
||||
|
||||
++lines_remaining_;
|
||||
if(!lines_remaining_) {
|
||||
should_reload_line_parameters_ = true;
|
||||
|
||||
// Check for end-of-frame.
|
||||
if(reload_line_parameter_pointer_) {
|
||||
line_parameter_pointer_ = line_parameter_base_;
|
||||
} else {
|
||||
line_parameter_pointer_ += 16;
|
||||
}
|
||||
} else {
|
||||
should_reload_line_parameters_ = false;
|
||||
}
|
||||
|
||||
// Deal with VRES and other address reloading, dependant upon mode.
|
||||
switch(mode_) {
|
||||
default: break;
|
||||
case Mode::CH64:
|
||||
case Mode::CH128:
|
||||
case Mode::CH256:
|
||||
line_data_pointer_[0] = start_line_data_pointer_[0];
|
||||
++line_data_pointer_[1];
|
||||
break;
|
||||
|
||||
case Mode::Pixel:
|
||||
case Mode::LPixel:
|
||||
case Mode::Attr:
|
||||
// Reload the pixel or attribute address if VRES is clear.
|
||||
if(!vres_) {
|
||||
line_data_pointer_[0] = start_line_data_pointer_[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#undef add_window
|
||||
|
||||
}
|
||||
|
||||
void Nick::set_output_type(OutputType type, bool force_flush) {
|
||||
if(type == output_type_ && !force_flush) {
|
||||
return;
|
||||
}
|
||||
if(output_duration_) {
|
||||
switch(output_type_) {
|
||||
case OutputType::Border: {
|
||||
uint16_t *const colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1));
|
||||
if(colour_pointer) *colour_pointer = border_colour_;
|
||||
crt_.output_level(output_duration_*16);
|
||||
} break;
|
||||
|
||||
case OutputType::Pixels: {
|
||||
crt_.output_data(output_duration_*16, size_t(output_duration_*column_size_));
|
||||
pixel_pointer_ = nullptr;
|
||||
allocated_pointer_ = nullptr;
|
||||
} break;
|
||||
|
||||
case OutputType::Sync: crt_.output_sync(output_duration_*16); break;
|
||||
case OutputType::Blank: crt_.output_blank(output_duration_*16); break;
|
||||
case OutputType::ColourBurst: crt_.output_colour_burst(output_duration_*16, 0); break;
|
||||
}
|
||||
}
|
||||
|
||||
output_duration_ = 0;
|
||||
output_type_ = type;
|
||||
}
|
||||
|
||||
// MARK: - Sequence points.
|
||||
|
||||
Cycles Nick::get_next_sequence_point() const {
|
||||
constexpr int load_point = 16; // i.e. 16 cycles after the start of the line, the
|
||||
// interrupt line may change. That is, after the
|
||||
// second byte of the mode line has been read.
|
||||
|
||||
// Any mode line may cause a change in the interrupt output, so as a first blush
|
||||
// just always report the time until the end of the mode line.
|
||||
if(lines_remaining_ || horizontal_counter_ >= load_point) {
|
||||
return Cycles(load_point + (912 - horizontal_counter_) + (0xff - lines_remaining_) * 912);
|
||||
} else {
|
||||
return Cycles(load_point - horizontal_counter_);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CRT passthroughs.
|
||||
|
||||
void Nick::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus Nick::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void Nick::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
first_pixel_window_ = display_type == Outputs::Display::DisplayType::RGB ? 8 : 10;
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType Nick::get_display_type() const {
|
||||
return crt_.get_display_type();
|
||||
}
|
||||
|
||||
// MARK: - Specific pixel outputters.
|
||||
|
||||
#define output1bpp(x) \
|
||||
target[0] = palette[(x & 0x80) >> 7]; \
|
||||
target[1] = palette[(x & 0x40) >> 6]; \
|
||||
target[2] = palette[(x & 0x20) >> 5]; \
|
||||
target[3] = palette[(x & 0x10) >> 4]; \
|
||||
target[4] = palette[(x & 0x08) >> 3]; \
|
||||
target[5] = palette[(x & 0x04) >> 2]; \
|
||||
target[6] = palette[(x & 0x02) >> 1]; \
|
||||
target[7] = palette[(x & 0x01) >> 0]; \
|
||||
target += 8
|
||||
|
||||
#define output2bpp(x) \
|
||||
target[0] = palette_[((x & 0x80) >> 7) | ((x & 0x08) >> 2)]; \
|
||||
target[1] = palette_[((x & 0x40) >> 6) | ((x & 0x04) >> 1)]; \
|
||||
target[2] = palette_[((x & 0x20) >> 5) | ((x & 0x02) >> 0)]; \
|
||||
target[3] = palette_[((x & 0x10) >> 4) | ((x & 0x01) << 1)]; \
|
||||
target += 4
|
||||
|
||||
#define output4bpp(x) \
|
||||
target[0] = palette_[((x & 0x02) << 2) | ((x & 0x20) >> 3) | ((x & 0x08) >> 2) | ((x & 0x80) >> 7)]; \
|
||||
target[1] = palette_[((x & 0x01) << 3) | ((x & 0x10) >> 2) | ((x & 0x04) >> 1) | ((x & 0x40) >> 6)]; \
|
||||
target += 2
|
||||
|
||||
#define output8bpp(x) \
|
||||
target[0] = mapped_colour(x); \
|
||||
++target
|
||||
|
||||
template <int bpp, bool is_lpixel> void Nick::output_pixel(uint16_t *target, int columns) const {
|
||||
static_assert(bpp == 1 || bpp == 2 || bpp == 4 || bpp == 8);
|
||||
|
||||
int index = 0;
|
||||
for(int c = 0; c < columns; c++) {
|
||||
uint8_t pixels[2] = {
|
||||
ram_[(line_data_pointer_[0] + index) & 0xffff],
|
||||
ram_[(line_data_pointer_[0] + index + 1) & 0xffff]
|
||||
};
|
||||
index += is_lpixel ? 1 : 2;
|
||||
last_read_ = pixels[1];
|
||||
|
||||
switch(bpp) {
|
||||
default:
|
||||
case 1: {
|
||||
const uint16_t *palette = alt_ind_palettes[((pixels[0] >> 6) & 0x02) | (pixels[0]&1)];
|
||||
pixels[0] &= two_colour_mask_;
|
||||
output1bpp(pixels[0]);
|
||||
|
||||
if constexpr (!is_lpixel) {
|
||||
palette = alt_ind_palettes[((pixels[1] >> 6) & 0x02) | (pixels[1]&1)];
|
||||
pixels[1] &= two_colour_mask_;
|
||||
output1bpp(pixels[1]);
|
||||
}
|
||||
} break;
|
||||
|
||||
case 2:
|
||||
output2bpp(pixels[0]);
|
||||
if constexpr (!is_lpixel) {
|
||||
output2bpp(pixels[1]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 4:
|
||||
output4bpp(pixels[0]);
|
||||
if constexpr (!is_lpixel) {
|
||||
output4bpp(pixels[1]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 8:
|
||||
output8bpp(pixels[0]);
|
||||
if constexpr (!is_lpixel) {
|
||||
output8bpp(pixels[1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <int bpp, int index_bits> void Nick::output_character(uint16_t *target, int columns) const {
|
||||
static_assert(bpp == 1 || bpp == 2 || bpp == 4 || bpp == 8);
|
||||
|
||||
for(int c = 0; c < columns; c++) {
|
||||
const uint8_t character = ram_[(line_data_pointer_[0] + c) & 0xffff];
|
||||
const uint8_t pixels = ram_[(
|
||||
(line_data_pointer_[1] << index_bits) +
|
||||
(character & ((1 << index_bits) - 1))
|
||||
) & 0xffff];
|
||||
last_read_ = pixels;
|
||||
|
||||
switch(bpp) {
|
||||
default:
|
||||
assert(false);
|
||||
break;
|
||||
|
||||
case 1: {
|
||||
// This applies ALTIND0 and ALTIND1.
|
||||
const uint16_t *palette = alt_ind_palettes[character >> 6];
|
||||
output1bpp(pixels);
|
||||
} break;
|
||||
|
||||
case 2: output2bpp(pixels); break;
|
||||
case 4: output4bpp(pixels); break;
|
||||
case 8: output8bpp(pixels); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <int bpp> void Nick::output_attributed(uint16_t *target, int columns) const {
|
||||
static_assert(bpp == 1 || bpp == 2 || bpp == 4 || bpp == 8);
|
||||
|
||||
for(int c = 0; c < columns; c++) {
|
||||
const uint8_t pixels = ram_[(line_data_pointer_[1] + c) & 0xffff];
|
||||
const uint8_t attributes = ram_[(line_data_pointer_[0] + c) & 0xffff];
|
||||
last_read_ = pixels;
|
||||
|
||||
const uint16_t palette[2] = {
|
||||
palette_[attributes >> 4], palette_[attributes & 0x0f]
|
||||
};
|
||||
output1bpp(pixels);
|
||||
}
|
||||
}
|
||||
|
||||
#undef output1bpp
|
||||
#undef output2bpp
|
||||
#undef output4bpp
|
||||
#undef output8bpp
|
||||
126
Machines/Enterprise/Nick.hpp
Normal file
126
Machines/Enterprise/Nick.hpp
Normal file
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// Nick.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 14/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Nick_hpp
|
||||
#define Nick_hpp
|
||||
|
||||
#include <cstdint>
|
||||
#include "../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../Outputs/CRT/CRT.hpp"
|
||||
|
||||
namespace Enterprise {
|
||||
|
||||
class Nick {
|
||||
public:
|
||||
Nick(const uint8_t *ram);
|
||||
|
||||
/// Writes to a Nick register; only the low two bits are decoded.
|
||||
void write(uint16_t address, uint8_t value);
|
||||
|
||||
/// Reads from the Nick range. Nobody seems to be completely clear what
|
||||
/// this should return; I've set it up to return the last fetched video or mode
|
||||
/// line byte during periods when those things are being fetched, 0xff at all
|
||||
/// other times. Including during refresh, since I don't know what addresses
|
||||
/// are generated then.
|
||||
///
|
||||
/// This likely isn't accurate, but is the most accurate guess I could make.
|
||||
uint8_t read();
|
||||
|
||||
void run_for(Cycles);
|
||||
Cycles get_time_until_z80_slot(Cycles after_period) const;
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/// @returns The amount of time until the next potential change in interrupt output.
|
||||
Cycles get_next_sequence_point() const;
|
||||
|
||||
/*!
|
||||
@returns The current state of the interrupt line — @c true for active;
|
||||
@c false for inactive.
|
||||
*/
|
||||
inline bool get_interrupt_line() const {
|
||||
return interrupt_line_;
|
||||
}
|
||||
|
||||
/// Sets the type of output.
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
/// Gets the type of output.
|
||||
Outputs::Display::DisplayType get_display_type() const;
|
||||
|
||||
private:
|
||||
Outputs::CRT::CRT crt_;
|
||||
const uint8_t *const ram_;
|
||||
|
||||
// CPU-provided state.
|
||||
uint8_t line_parameter_control_ = 0xc0;
|
||||
uint16_t line_parameter_base_ = 0x0000;
|
||||
uint16_t border_colour_ = 0;
|
||||
|
||||
// Ephemerals, related to current video position.
|
||||
int horizontal_counter_ = 0;
|
||||
uint16_t line_parameter_pointer_ = 0x0000;
|
||||
bool should_reload_line_parameters_ = true;
|
||||
uint16_t line_data_pointer_[2];
|
||||
uint16_t start_line_data_pointer_[2];
|
||||
mutable uint8_t last_read_ = 0xff;
|
||||
|
||||
// Current mode line parameters.
|
||||
uint8_t lines_remaining_ = 0x00;
|
||||
uint8_t two_colour_mask_ = 0xff;
|
||||
int left_margin_ = 0, right_margin_ = 0;
|
||||
const uint16_t *alt_ind_palettes[4];
|
||||
enum class Mode {
|
||||
Vsync,
|
||||
Pixel,
|
||||
Attr,
|
||||
CH256,
|
||||
CH128,
|
||||
CH64,
|
||||
Unused,
|
||||
LPixel,
|
||||
} mode_ = Mode::Vsync;
|
||||
bool is_sync_or_pixels_ = false;
|
||||
int bpp_ = 0;
|
||||
int column_size_ = 0;
|
||||
bool interrupt_line_ = true;
|
||||
int line_data_per_column_increments_[2] = {0, 0};
|
||||
bool vres_ = false;
|
||||
bool reload_line_parameter_pointer_ = false;
|
||||
|
||||
// The destination for new pixels.
|
||||
static constexpr int allocation_size = 336;
|
||||
static_assert((allocation_size % 16) == 0, "Allocation size must be a multiple of 16");
|
||||
uint16_t *pixel_pointer_ = nullptr, *allocated_pointer_ = nullptr;
|
||||
|
||||
// Output transitions.
|
||||
enum class OutputType {
|
||||
Sync, Blank, Pixels, Border, ColourBurst
|
||||
};
|
||||
void set_output_type(OutputType, bool force_flush = false);
|
||||
int output_duration_ = 0;
|
||||
OutputType output_type_ = OutputType::Sync;
|
||||
|
||||
// Current palette.
|
||||
uint16_t palette_[16]{};
|
||||
|
||||
// The first column with pixels on it; will be either 8 or 10 depending
|
||||
// on whether the colour burst is meaningful to the current display type.
|
||||
int first_pixel_window_ = 10;
|
||||
|
||||
// Specific outputters.
|
||||
template <int bpp, bool is_lpixel> void output_pixel(uint16_t *target, int columns) const;
|
||||
template <int bpp, int index_bits> void output_character(uint16_t *target, int columns) const;
|
||||
template <int bpp> void output_attributed(uint16_t *target, int columns) const;
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endif /* Nick_hpp */
|
||||
@@ -169,19 +169,22 @@ class ConcreteMachine:
|
||||
|
||||
// Install the proper TV standard and select an ideal BIOS name.
|
||||
const std::string machine_name = "MSX";
|
||||
std::vector<ROMMachine::ROM> required_roms = {
|
||||
{machine_name, "any MSX BIOS", "msx.rom", 32*1024, 0x94ee12f3}
|
||||
};
|
||||
ROM::Request bios_request = ROM::Request(ROM::Name::MSXGenericBIOS);
|
||||
// std::vector<ROMMachine::ROM> required_roms = {
|
||||
// {machine_name, "any MSX BIOS", "msx.rom", 32*1024, 0x94ee12f3u}
|
||||
// };
|
||||
|
||||
bool is_ntsc = true;
|
||||
uint8_t character_generator = 1; /* 0 = Japan, 1 = USA, etc, 2 = USSR */
|
||||
uint8_t date_format = 1; /* 0 = Y/M/D, 1 = M/D/Y, 2 = D/M/Y */
|
||||
uint8_t keyboard = 1; /* 0 = Japan, 1 = USA, 2 = France, 3 = UK, 4 = Germany, 5 = USSR, 6 = Spain */
|
||||
ROM::Name regional_bios_name;
|
||||
|
||||
// TODO: CRCs below are incomplete, at best.
|
||||
switch(target.region) {
|
||||
default:
|
||||
case Target::Region::Japan:
|
||||
required_roms.emplace_back(machine_name, "a Japanese MSX BIOS", "msx-japanese.rom", 32*1024, 0xee229390);
|
||||
regional_bios_name = ROM::Name::MSXJapaneseBIOS;
|
||||
vdp_->set_tv_standard(TI::TMS::TVStandard::NTSC);
|
||||
|
||||
is_ntsc = true;
|
||||
@@ -189,7 +192,7 @@ class ConcreteMachine:
|
||||
date_format = 0;
|
||||
break;
|
||||
case Target::Region::USA:
|
||||
required_roms.emplace_back(machine_name, "an American MSX BIOS", "msx-american.rom", 32*1024, 0);
|
||||
regional_bios_name = ROM::Name::MSXAmericanBIOS;
|
||||
vdp_->set_tv_standard(TI::TMS::TVStandard::NTSC);
|
||||
|
||||
is_ntsc = true;
|
||||
@@ -197,7 +200,7 @@ class ConcreteMachine:
|
||||
date_format = 1;
|
||||
break;
|
||||
case Target::Region::Europe:
|
||||
required_roms.emplace_back(machine_name, "a European MSX BIOS", "msx-european.rom", 32*1024, 0);
|
||||
regional_bios_name = ROM::Name::MSXEuropeanBIOS;
|
||||
vdp_->set_tv_standard(TI::TMS::TVStandard::PAL);
|
||||
|
||||
is_ntsc = false;
|
||||
@@ -205,27 +208,30 @@ class ConcreteMachine:
|
||||
date_format = 2;
|
||||
break;
|
||||
}
|
||||
bios_request = bios_request || ROM::Request(regional_bios_name);
|
||||
|
||||
// Fetch the necessary ROMs; try the region-specific ROM first,
|
||||
// but failing that fall back on patching the main one.
|
||||
size_t disk_index = 0;
|
||||
ROM::Request request;
|
||||
if(target.has_disk_drive) {
|
||||
disk_index = required_roms.size();
|
||||
required_roms.emplace_back(machine_name, "the MSX-DOS ROM", "disk.rom", 16*1024, 0x721f61df);
|
||||
request = ROM::Request(ROM::Name::MSXDOS) && bios_request;
|
||||
} else {
|
||||
request = bios_request;
|
||||
}
|
||||
const auto roms = rom_fetcher(required_roms);
|
||||
|
||||
if((!roms[0] && !roms[1]) || (target.has_disk_drive && !roms[2])) {
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
// Figure out which BIOS to use, either a specific one or the generic
|
||||
// one appropriately patched.
|
||||
if(roms[1]) {
|
||||
memory_slots_[0].source = std::move(*roms[1]);
|
||||
const auto regional_bios = roms.find(regional_bios_name);
|
||||
if(regional_bios != roms.end()) {
|
||||
memory_slots_[0].source = std::move(regional_bios->second);
|
||||
memory_slots_[0].source.resize(32768);
|
||||
} else {
|
||||
memory_slots_[0].source = std::move(*roms[0]);
|
||||
memory_slots_[0].source = std::move(roms.find(ROM::Name::MSXGenericBIOS)->second);
|
||||
memory_slots_[0].source.resize(32768);
|
||||
|
||||
memory_slots_[0].source[0x2b] = uint8_t(
|
||||
@@ -252,7 +258,7 @@ class ConcreteMachine:
|
||||
// Add a disk cartridge if any disks were supplied.
|
||||
if(target.has_disk_drive) {
|
||||
memory_slots_[2].set_handler(new DiskROM(memory_slots_[2].source));
|
||||
memory_slots_[2].source = std::move(*roms[disk_index]);
|
||||
memory_slots_[2].source = std::move(roms.find(ROM::Name::MSXDOS)->second);
|
||||
memory_slots_[2].source.resize(16384);
|
||||
|
||||
map(2, 0, 0x4000, 0x2000);
|
||||
@@ -419,7 +425,9 @@ class ConcreteMachine:
|
||||
// but otherwise runs without pause.
|
||||
const HalfCycles addition((cycle.operation == CPU::Z80::PartialMachineCycle::ReadOpcode) ? 2 : 0);
|
||||
const HalfCycles total_length = addition + cycle.length;
|
||||
vdp_ += total_length;
|
||||
if(vdp_ += total_length) {
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
|
||||
}
|
||||
time_since_ay_update_ += total_length;
|
||||
memory_slots_[0].cycles_since_update += total_length;
|
||||
memory_slots_[1].cycles_since_update += total_length;
|
||||
@@ -520,14 +528,11 @@ class ConcreteMachine:
|
||||
case 0x98: case 0x99:
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 0xa2:
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
*cycle.value = ay_.get_data_output();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
*cycle.value = GI::AY38910::Utility::read(ay_);
|
||||
break;
|
||||
|
||||
case 0xa8: case 0xa9:
|
||||
@@ -547,14 +552,11 @@ class ConcreteMachine:
|
||||
case 0x98: case 0x99:
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 0xa0: case 0xa1:
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | ((port == 0xa0) ? GI::AY38910::BC1 : 0)));
|
||||
ay_.set_data_input(*cycle.value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
GI::AY38910::Utility::write(ay_, port == 0xa1, *cycle.value);
|
||||
break;
|
||||
|
||||
case 0xa8: case 0xa9:
|
||||
@@ -610,12 +612,6 @@ class ConcreteMachine:
|
||||
if(!tape_player_is_sleeping_)
|
||||
tape_player_.run_for(int(cycle.length.as_integral()));
|
||||
|
||||
if(time_until_interrupt_ > 0) {
|
||||
time_until_interrupt_ -= total_length;
|
||||
if(time_until_interrupt_ <= HalfCycles(0)) {
|
||||
z80_.set_interrupt_line(true, time_until_interrupt_);
|
||||
}
|
||||
}
|
||||
return addition;
|
||||
}
|
||||
|
||||
@@ -789,7 +785,6 @@ class ConcreteMachine:
|
||||
uint8_t unpopulated_[8192];
|
||||
|
||||
HalfCycles time_since_ay_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
|
||||
uint8_t key_states_[16];
|
||||
int selected_key_line_ = 0;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "MediaTarget.hpp"
|
||||
#include "MouseMachine.hpp"
|
||||
#include "ScanProducer.hpp"
|
||||
#include "StateProducer.hpp"
|
||||
#include "TimedMachine.hpp"
|
||||
|
||||
#endif /* MachineTypes_h */
|
||||
|
||||
@@ -141,23 +141,20 @@ class ConcreteMachine:
|
||||
// 0072ed54 = US/European BIOS 1.3
|
||||
// 48d44a13 = Japanese BIOS 2.1
|
||||
const bool is_japanese = target.region == Target::Region::Japan;
|
||||
const auto roms = rom_fetcher(
|
||||
{ {"MasterSystem",
|
||||
is_japanese ? "the Japanese Master System BIOS" : "the European/US Master System BIOS",
|
||||
is_japanese ? "japanese-bios.sms" : "bios.sms",
|
||||
8*1024,
|
||||
{ is_japanese ? 0x48d44a13u : 0x0072ed54u }
|
||||
} }
|
||||
);
|
||||
if(!roms[0]) {
|
||||
const ROM::Name bios_name = is_japanese ? ROM::Name::MasterSystemJapaneseBIOS : ROM::Name::MasterSystemWesternBIOS;
|
||||
ROM::Request request(bios_name, true);
|
||||
auto roms = rom_fetcher(request);
|
||||
request.validate(roms);
|
||||
|
||||
const auto rom = roms.find(bios_name);
|
||||
if(rom == roms.end()) {
|
||||
// No BIOS found; attempt to boot as though it has already disabled itself.
|
||||
has_bios_ = false;
|
||||
memory_control_ |= 0x08;
|
||||
std::cerr << "No BIOS found; attempting to start cartridge directly" << std::endl;
|
||||
} else {
|
||||
has_bios_ = true;
|
||||
roms[0]->resize(8*1024);
|
||||
memcpy(&bios_, roms[0]->data(), roms[0]->size());
|
||||
memcpy(&bios_, rom->second.data(), std::min(sizeof(bios_), rom->second.size()));
|
||||
}
|
||||
page_cartridge();
|
||||
|
||||
@@ -214,7 +211,9 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
vdp_ += cycle.length;
|
||||
if(vdp_ += cycle.length) {
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
|
||||
}
|
||||
time_since_sn76489_update_ += cycle.length;
|
||||
|
||||
if(cycle.is_terminal()) {
|
||||
@@ -266,7 +265,6 @@ class ConcreteMachine:
|
||||
case 0x80: case 0x81:
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
case 0xc0: {
|
||||
if(memory_control_ & 0x4) {
|
||||
@@ -330,7 +328,6 @@ class ConcreteMachine:
|
||||
case 0x80: case 0x81: // i.e. ports 0x80–0xbf.
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
case 0xc1: case 0xc0: // i.e. ports 0xc0–0xff.
|
||||
if(has_fm_audio_) {
|
||||
@@ -374,13 +371,6 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
if(time_until_interrupt_ > 0) {
|
||||
time_until_interrupt_ -= cycle.length;
|
||||
if(time_until_interrupt_ <= HalfCycles(0)) {
|
||||
z80_.set_interrupt_line(true, time_until_interrupt_);
|
||||
}
|
||||
}
|
||||
|
||||
// The pause button is debounced and takes effect only one line before pixels
|
||||
// begin; time_until_debounce_ keeps track of the time until then.
|
||||
time_until_debounce_ -= cycle.length;
|
||||
@@ -505,7 +495,6 @@ class ConcreteMachine:
|
||||
bool reset_is_pressed_ = false, pause_is_pressed_ = false;
|
||||
|
||||
HalfCycles time_since_sn76489_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
HalfCycles time_until_debounce_;
|
||||
|
||||
uint8_t ram_[8*1024];
|
||||
|
||||
@@ -127,5 +127,5 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
#undef SHIFT
|
||||
#undef X
|
||||
|
||||
return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
|
||||
return table_lookup_sequence_for_character(key_sequences, character);
|
||||
}
|
||||
|
||||
@@ -34,10 +34,51 @@
|
||||
|
||||
#include "../../Analyser/Static/Oric/Target.hpp"
|
||||
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
/*!
|
||||
Provides an Altai-style joystick.
|
||||
*/
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Up),
|
||||
Input(Input::Down),
|
||||
Input(Input::Left),
|
||||
Input(Input::Right),
|
||||
Input(Input::Fire)
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
#define APPLY(b) if(is_active) state_ &= ~b; else state_ |= b;
|
||||
switch(digital_input.type) {
|
||||
default: return;
|
||||
case Input::Right: APPLY(0x02); break;
|
||||
case Input::Left: APPLY(0x01); break;
|
||||
case Input::Down: APPLY(0x08); break;
|
||||
case Input::Up: APPLY(0x10); break;
|
||||
case Input::Fire: APPLY(0x20); break;
|
||||
}
|
||||
#undef APPLY
|
||||
}
|
||||
|
||||
uint8_t get_state() {
|
||||
return state_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t state_ = 0xff;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace Oric {
|
||||
|
||||
using DiskInterface = Analyser::Static::Oric::Target::DiskInterface;
|
||||
@@ -138,7 +179,12 @@ class TapePlayer: public Storage::Tape::BinaryTapePlayer {
|
||||
class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
public:
|
||||
VIAPortHandler(Concurrency::DeferringAsyncTaskQueue &audio_queue, AY &ay8910, Speaker &speaker, TapePlayer &tape_player, Keyboard &keyboard) :
|
||||
audio_queue_(audio_queue), ay8910_(ay8910), speaker_(speaker), tape_player_(tape_player), keyboard_(keyboard) {}
|
||||
audio_queue_(audio_queue), ay8910_(ay8910), speaker_(speaker), tape_player_(tape_player), keyboard_(keyboard)
|
||||
{
|
||||
// Attach a couple of joysticks.
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
}
|
||||
|
||||
/*!
|
||||
Reponds to the 6522's control line output change signal; on an Oric A2 is connected to
|
||||
@@ -163,6 +209,7 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
} else {
|
||||
update_ay();
|
||||
ay8910_.set_data_input(value);
|
||||
porta_output_ = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +221,10 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
uint8_t column = ay8910_.get_port_output(false) ^ 0xff;
|
||||
return keyboard_.query_column(column) ? 0x08 : 0x00;
|
||||
} else {
|
||||
return ay8910_.get_data_output();
|
||||
uint8_t result = ay8910_.get_data_output();
|
||||
if(porta_output_ & 0x40) result &= static_cast<Joystick *>(joysticks_[0].get())->get_state();
|
||||
if(porta_output_ & 0x80) result &= static_cast<Joystick *>(joysticks_[1].get())->get_state();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,12 +241,17 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
private:
|
||||
void update_ay() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_ay_update_.flush<Cycles>());
|
||||
}
|
||||
bool ay_bdir_ = false;
|
||||
bool ay_bc1_ = false;
|
||||
uint8_t porta_output_ = 0xff;
|
||||
HalfCycles cycles_since_ay_update_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
@@ -204,12 +259,15 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
Speaker &speaker_;
|
||||
TapePlayer &tape_player_;
|
||||
Keyboard &keyboard_;
|
||||
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
};
|
||||
|
||||
template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS6502Esque::Type processor_type> class ConcreteMachine:
|
||||
public MachineTypes::TimedMachine,
|
||||
public MachineTypes::ScanProducer,
|
||||
public MachineTypes::AudioProducer,
|
||||
public MachineTypes::JoystickMachine,
|
||||
public MachineTypes::MediaTarget,
|
||||
public MachineTypes::MappedKeyboardMachine,
|
||||
public Configurable::Device,
|
||||
@@ -217,7 +275,6 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
|
||||
public Storage::Tape::BinaryTapePlayer::Delegate,
|
||||
public DiskController::Delegate,
|
||||
public ClockingHint::Observer,
|
||||
public Activity::Source,
|
||||
public Machine,
|
||||
public Keyboard::SpecialKeyHandler {
|
||||
@@ -225,7 +282,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::Oric::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
m6502_(*this),
|
||||
video_output_(ram_),
|
||||
video_(ram_),
|
||||
ay8910_(GI::AY38910::Personality::AY38910, audio_queue_),
|
||||
speaker_(ay8910_),
|
||||
via_port_handler_(audio_queue_, ay8910_, speaker_, tape_player_, keyboard_),
|
||||
@@ -247,77 +304,64 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
ram_[c] |= 0x40;
|
||||
}
|
||||
|
||||
if constexpr (disk_interface == DiskInterface::Pravetz) {
|
||||
diskii_.set_clocking_hint_observer(this);
|
||||
}
|
||||
|
||||
const std::string machine_name = "Oric";
|
||||
std::vector<ROMMachine::ROM> rom_names = { {machine_name, "the Oric colour ROM", "colour.rom", 128, 0xd50fca65} };
|
||||
::ROM::Request request = ::ROM::Request(::ROM::Name::OricColourROM, true);
|
||||
::ROM::Name basic;
|
||||
switch(target.rom) {
|
||||
case Analyser::Static::Oric::Target::ROM::BASIC10:
|
||||
rom_names.emplace_back(machine_name, "Oric BASIC 1.0", "basic10.rom", 16*1024, 0xf18710b4);
|
||||
break;
|
||||
case Analyser::Static::Oric::Target::ROM::BASIC11:
|
||||
rom_names.emplace_back(machine_name, "Oric BASIC 1.1", "basic11.rom", 16*1024, 0xc3a92bef);
|
||||
break;
|
||||
case Analyser::Static::Oric::Target::ROM::Pravetz:
|
||||
rom_names.emplace_back(machine_name, "Pravetz BASIC", "pravetz.rom", 16*1024, 0x58079502);
|
||||
break;
|
||||
case Analyser::Static::Oric::Target::ROM::BASIC10: basic = ::ROM::Name::OricBASIC10; break;
|
||||
default:
|
||||
case Analyser::Static::Oric::Target::ROM::BASIC11: basic = ::ROM::Name::OricBASIC11; break;
|
||||
case Analyser::Static::Oric::Target::ROM::Pravetz: basic = ::ROM::Name::OricPravetzBASIC; break;
|
||||
}
|
||||
size_t diskii_state_machine_index = 0;
|
||||
request = request && ::ROM::Request(basic);
|
||||
|
||||
switch(disk_interface) {
|
||||
default: break;
|
||||
case DiskInterface::BD500:
|
||||
rom_names.emplace_back(machine_name, "the Oric Byte Drive 500 ROM", "bd500.rom", 8*1024, 0x61952e34);
|
||||
request = request && ::ROM::Request(::ROM::Name::OricByteDrive500);
|
||||
break;
|
||||
case DiskInterface::Jasmin:
|
||||
rom_names.emplace_back(machine_name, "the Oric Jasmin ROM", "jasmin.rom", 2*1024, 0x37220e89);
|
||||
request = request && ::ROM::Request(::ROM::Name::OricJasmin);
|
||||
break;
|
||||
case DiskInterface::Microdisc:
|
||||
rom_names.emplace_back(machine_name, "the Oric Microdisc ROM", "microdisc.rom", 8*1024, 0xa9664a9c);
|
||||
request = request && ::ROM::Request(::ROM::Name::OricMicrodisc);
|
||||
break;
|
||||
case DiskInterface::Pravetz:
|
||||
rom_names.emplace_back(machine_name, "the 8DOS boot ROM", "8dos.rom", 512, 0x49a74c06);
|
||||
// These ROM details are coupled with those in the DiskIICard.
|
||||
diskii_state_machine_index = rom_names.size();
|
||||
rom_names.push_back({"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 }});
|
||||
request = request && ::ROM::Request(::ROM::Name::Oric8DOSBoot) && ::ROM::Request(::ROM::Name::DiskIIStateMachine16Sector);
|
||||
break;
|
||||
}
|
||||
|
||||
const auto roms = rom_fetcher(rom_names);
|
||||
|
||||
for(std::size_t index = 0; index < roms.size(); ++index) {
|
||||
if(!roms[index]) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
video_output_.set_colour_rom(*roms[0]);
|
||||
rom_ = std::move(*roms[1]);
|
||||
// The colour ROM is optional; an alternative composite encoding can be used if
|
||||
// it is absent.
|
||||
const auto colour_rom = roms.find(::ROM::Name::OricColourROM);
|
||||
if(colour_rom != roms.end()) {
|
||||
video_->set_colour_rom(colour_rom->second);
|
||||
}
|
||||
rom_ = std::move(roms.find(basic)->second);
|
||||
|
||||
switch(disk_interface) {
|
||||
default: break;
|
||||
case DiskInterface::BD500:
|
||||
disk_rom_ = std::move(*roms[2]);
|
||||
disk_rom_.resize(8192);
|
||||
disk_rom_ = std::move(roms.find(::ROM::Name::OricByteDrive500)->second);
|
||||
break;
|
||||
case DiskInterface::Jasmin:
|
||||
disk_rom_ = std::move(*roms[2]);
|
||||
disk_rom_.resize(2048);
|
||||
disk_rom_ = std::move(roms.find(::ROM::Name::OricJasmin)->second);
|
||||
break;
|
||||
case DiskInterface::Microdisc:
|
||||
disk_rom_ = std::move(*roms[2]);
|
||||
disk_rom_.resize(8192);
|
||||
disk_rom_ = std::move(roms.find(::ROM::Name::OricMicrodisc)->second);
|
||||
break;
|
||||
case DiskInterface::Pravetz: {
|
||||
pravetz_rom_ = std::move(*roms[2]);
|
||||
pravetz_rom_ = std::move(roms.find(::ROM::Name::Oric8DOSBoot)->second);
|
||||
pravetz_rom_.resize(512);
|
||||
|
||||
diskii_.set_state_machine(*roms[diskii_state_machine_index]);
|
||||
diskii_->set_state_machine(roms.find(::ROM::Name::DiskIIStateMachine16Sector)->second);
|
||||
} break;
|
||||
}
|
||||
|
||||
rom_.resize(16384);
|
||||
paged_rom_ = rom_.data();
|
||||
|
||||
switch(target.disk_interface) {
|
||||
@@ -402,7 +446,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
case DiskInterface::BD500: inserted |= insert_disks(media, bd500_, 4); break;
|
||||
case DiskInterface::Jasmin: inserted |= insert_disks(media, jasmin_, 4); break;
|
||||
case DiskInterface::Microdisc: inserted |= insert_disks(media, microdisc_, 4); break;
|
||||
case DiskInterface::Pravetz: inserted |= insert_disks(media, diskii_, 2); break;
|
||||
case DiskInterface::Pravetz: inserted |= insert_disks(media, *diskii_.last_valid(), 2); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -474,9 +518,8 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flush_diskii();
|
||||
const int disk_value = diskii_.read_address(address);
|
||||
if(!isWriteOperation(operation) && disk_value != diskii_.DidNotLoad) *value = uint8_t(disk_value);
|
||||
const int disk_value = diskii_->read_address(address);
|
||||
if(!isWriteOperation(operation) && disk_value != Apple::DiskII::DidNotLoad) *value = uint8_t(disk_value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -485,7 +528,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
if(!isWriteOperation(operation))
|
||||
*value = ram_[address];
|
||||
else {
|
||||
if(address >= 0x9800 && address <= 0xc000) update_video();
|
||||
if(address >= 0x9800 && address <= 0xc000) video_.flush();
|
||||
ram_[address] = *value;
|
||||
}
|
||||
}
|
||||
@@ -508,7 +551,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
bd500_.run_for(Cycles(9)); // i.e. effective clock rate of 9Mhz.
|
||||
break;
|
||||
case DiskInterface::Jasmin:
|
||||
jasmin_.run_for(Cycles(8));; // i.e. effective clock rate of 8Mhz.
|
||||
jasmin_.run_for(Cycles(8)); // i.e. effective clock rate of 8Mhz.
|
||||
|
||||
// Jasmin autostart hack: wait for a period, then trigger a reset, having forced
|
||||
// the Jasmin to page its ROM in first. I assume the latter being what the Jasmin's
|
||||
@@ -521,43 +564,41 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
}
|
||||
break;
|
||||
case DiskInterface::Microdisc:
|
||||
microdisc_.run_for(Cycles(8));; // i.e. effective clock rate of 8Mhz.
|
||||
microdisc_.run_for(Cycles(8)); // i.e. effective clock rate of 8Mhz.
|
||||
break;
|
||||
case DiskInterface::Pravetz:
|
||||
if(diskii_clocking_preference_ == ClockingHint::Preference::RealTime) {
|
||||
diskii_.set_data_input(*value);
|
||||
diskii_.run_for(Cycles(2));; // i.e. effective clock rate of 2Mhz.
|
||||
} else {
|
||||
cycles_since_diskii_update_ += Cycles(2);
|
||||
if(diskii_.clocking_preference() == ClockingHint::Preference::RealTime) {
|
||||
diskii_->set_data_input(*value);
|
||||
}
|
||||
diskii_ += Cycles(2); // i.e. effective clock rate of 2Mhz.
|
||||
break;
|
||||
}
|
||||
cycles_since_video_update_++;
|
||||
|
||||
video_ += Cycles(1);
|
||||
return Cycles(1);
|
||||
}
|
||||
|
||||
forceinline void flush() {
|
||||
update_video();
|
||||
video_.flush();
|
||||
via_.flush();
|
||||
flush_diskii();
|
||||
diskii_.flush();
|
||||
}
|
||||
|
||||
// to satisfy CRTMachine::Machine
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_output_.set_scan_target(scan_target);
|
||||
video_.last_valid()->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_output_.get_scaled_scan_status();
|
||||
return video_.last_valid()->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_output_.set_display_type(display_type);
|
||||
video_.last_valid()->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType get_display_type() const final {
|
||||
return video_output_.get_display_type();
|
||||
return video_.last_valid()->get_display_type();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
@@ -644,15 +685,11 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
microdisc_.set_activity_observer(observer);
|
||||
break;
|
||||
case DiskInterface::Pravetz:
|
||||
diskii_.set_activity_observer(observer);
|
||||
diskii_->set_activity_observer(observer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) final {
|
||||
diskii_clocking_preference_ = diskii_.preferred_clocking();
|
||||
}
|
||||
|
||||
private:
|
||||
const uint16_t basic_invisible_ram_top_ = 0xffff;
|
||||
const uint16_t basic_visible_ram_top_ = 0xbfff;
|
||||
@@ -662,17 +699,13 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
// RAM and ROM
|
||||
std::vector<uint8_t> rom_, disk_rom_;
|
||||
uint8_t ram_[65536];
|
||||
Cycles cycles_since_video_update_;
|
||||
inline void update_video() {
|
||||
video_output_.run_for(cycles_since_video_update_.flush<Cycles>());
|
||||
}
|
||||
|
||||
// ROM bookkeeping
|
||||
uint16_t tape_get_byte_address_ = 0, tape_speed_address_ = 0;
|
||||
int keyboard_read_count_ = 0;
|
||||
|
||||
// Outputs
|
||||
VideoOutput video_output_;
|
||||
JustInTimeActor<VideoOutput, Cycles> video_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910<false> ay8910_;
|
||||
@@ -700,14 +733,9 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
BD500 bd500_;
|
||||
|
||||
// the Pravetz/Disk II, if in use.
|
||||
Apple::DiskII diskii_;
|
||||
Cycles cycles_since_diskii_update_;
|
||||
void flush_diskii() {
|
||||
diskii_.run_for(cycles_since_diskii_update_.flush<Cycles>());
|
||||
}
|
||||
JustInTimeActor<Apple::DiskII, Cycles> diskii_;
|
||||
std::vector<uint8_t> pravetz_rom_;
|
||||
std::size_t pravetz_rom_base_pointer_ = 0;
|
||||
ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime;
|
||||
|
||||
// Overlay RAM
|
||||
uint16_t ram_top_ = basic_visible_ram_top_;
|
||||
@@ -750,8 +778,13 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
}
|
||||
}
|
||||
|
||||
// MARK - typing
|
||||
// MARK: - typing
|
||||
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
|
||||
|
||||
// MARK: - Joysticks
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
return via_port_handler_.get_joysticks();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
|
||||
#ifdef SUPPLY_COMPOSITE
|
||||
const auto data_type =
|
||||
(display_type == Outputs::Display::DisplayType::RGB) ?
|
||||
(!has_colour_rom_ || display_type == Outputs::Display::DisplayType::RGB) ?
|
||||
Outputs::Display::InputDataType::Red1Green1Blue1 :
|
||||
Outputs::Display::InputDataType::PhaseLinkedLuminance8;
|
||||
#else
|
||||
@@ -80,6 +80,7 @@ Outputs::Display::ScanStatus VideoOutput::get_scaled_scan_status() const {
|
||||
}
|
||||
|
||||
void VideoOutput::set_colour_rom(const std::vector<uint8_t> &rom) {
|
||||
has_colour_rom_ = true;
|
||||
for(std::size_t c = 0; c < 8; c++) {
|
||||
colour_forms_[c] = 0;
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ class VideoOutput {
|
||||
Outputs::CRT::CRT crt_;
|
||||
Outputs::CRT::CRTFrequencyMismatchWarner<VideoOutput> frequency_mismatch_warner_;
|
||||
bool crt_is_60Hz_ = false;
|
||||
bool has_colour_rom_ = false;
|
||||
|
||||
void update_crt_frequency();
|
||||
|
||||
|
||||
@@ -10,38 +10,16 @@
|
||||
#define ROMMachine_hpp
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Utility/ROMCatalogue.hpp"
|
||||
|
||||
namespace ROMMachine {
|
||||
|
||||
/*!
|
||||
Describes a ROM image; this term is used in this emulator strictly in the sense of firmware —
|
||||
system software that is an inherent part of a machine.
|
||||
*/
|
||||
struct ROM {
|
||||
/// The machine with which this ROM is associated, in a form that is safe for using as
|
||||
/// part of a file name.
|
||||
std::string machine_name;
|
||||
/// A descriptive name for this ROM, suitable for use in a bullet-point list, a bracket
|
||||
/// clause, etc, e.g. "the Electron MOS 1.0".
|
||||
std::string descriptive_name;
|
||||
/// An idiomatic file name for this ROM, e.g. "os10.rom".
|
||||
std::string file_name;
|
||||
/// The expected size of this ROM in bytes, e.g. 32768.
|
||||
size_t size = 0;
|
||||
/// CRC32s for all known acceptable copies of this ROM; intended to allow a host platform
|
||||
/// to test user-provided ROMs of unknown provenance. **Not** intended to be used
|
||||
/// to exclude ROMs where the user's intent is otherwise clear.
|
||||
std::vector<uint32_t> crc32s;
|
||||
|
||||
ROM(std::string machine_name, std::string descriptive_name, std::string file_name, size_t size, uint32_t crc32) :
|
||||
machine_name(machine_name), descriptive_name(descriptive_name), file_name(file_name), size(size), crc32s({crc32}) {}
|
||||
ROM(std::string machine_name, std::string descriptive_name, std::string file_name, size_t size, std::initializer_list<uint32_t> crc32s) :
|
||||
machine_name(machine_name), descriptive_name(descriptive_name), file_name(file_name), size(size), crc32s(crc32s) {}
|
||||
};
|
||||
|
||||
/*!
|
||||
Defines the signature for a function that must be supplied by the host environment in order to give machines
|
||||
a route for fetching any system ROMs they might need.
|
||||
@@ -50,7 +28,7 @@ struct ROM {
|
||||
return a vector of unique_ptrs that either contain the contents of the ROM from @c names that corresponds by
|
||||
index, or else are @c nullptr.
|
||||
*/
|
||||
typedef std::function<std::vector<std::unique_ptr<std::vector<uint8_t>>>(const std::vector<ROM> &roms)> ROMFetcher;
|
||||
typedef std::function<ROM::Map(const ROM::Request &request)> ROMFetcher;
|
||||
|
||||
enum class Error {
|
||||
MissingROMs
|
||||
|
||||
@@ -72,7 +72,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
#define SHIFT(...) {KeyShift, __VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
|
||||
#define SYMSHIFT(...) {KeySymbolShift, __VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
|
||||
#define X {MachineTypes::MappedKeyboardMachine::KeyNotMapped}
|
||||
constexpr KeySequence spectrum_key_sequences[] = {
|
||||
static constexpr KeySequence spectrum_key_sequences[] = {
|
||||
/* NUL */ X, /* SOH */ X,
|
||||
/* STX */ X, /* ETX */ X,
|
||||
/* EOT */ X, /* ENQ */ X,
|
||||
@@ -137,7 +137,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
/* z */ KEYS(KeyZ),
|
||||
};
|
||||
|
||||
constexpr KeySequence zx81_key_sequences[] = {
|
||||
static constexpr KeySequence zx81_key_sequences[] = {
|
||||
/* NUL */ X, /* SOH */ X,
|
||||
/* STX */ X, /* ETX */ X,
|
||||
/* EOT */ X, /* ENQ */ X,
|
||||
@@ -203,7 +203,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
/* | */ X, /* } */ X,
|
||||
};
|
||||
|
||||
static KeySequence zx80_key_sequences[] = {
|
||||
static constexpr KeySequence zx80_key_sequences[] = {
|
||||
/* NUL */ X, /* SOH */ X,
|
||||
/* STX */ X, /* ETX */ X,
|
||||
/* EOT */ X, /* ENQ */ X,
|
||||
@@ -275,13 +275,14 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
|
||||
switch(machine_) {
|
||||
case Machine::ZX80:
|
||||
return table_lookup_sequence_for_character(zx80_key_sequences, sizeof(zx80_key_sequences), character);
|
||||
return table_lookup_sequence_for_character(zx80_key_sequences, character);
|
||||
|
||||
case Machine::ZX81:
|
||||
return table_lookup_sequence_for_character(zx81_key_sequences, sizeof(zx81_key_sequences), character);
|
||||
return table_lookup_sequence_for_character(zx81_key_sequences, character);
|
||||
|
||||
default:
|
||||
case Machine::ZXSpectrum:
|
||||
return table_lookup_sequence_for_character(spectrum_key_sequences, sizeof(zx81_key_sequences), character);
|
||||
return table_lookup_sequence_for_character(spectrum_key_sequences, character);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,15 +74,13 @@ template<bool is_zx81> class ConcreteMachine:
|
||||
speaker_.set_input_rate(float(ZX8081ClockRate) / 2.0f);
|
||||
|
||||
const bool use_zx81_rom = target.is_ZX81 || target.ZX80_uses_ZX81_ROM;
|
||||
const auto roms =
|
||||
use_zx81_rom ?
|
||||
rom_fetcher({ {"ZX8081", "the ZX81 BASIC ROM", "zx81.rom", 8 * 1024, 0x4b1dd6eb} }) :
|
||||
rom_fetcher({ {"ZX8081", "the ZX80 BASIC ROM", "zx80.rom", 4 * 1024, 0x4c7fc597} });
|
||||
|
||||
if(!roms[0]) throw ROMMachine::Error::MissingROMs;
|
||||
|
||||
rom_ = std::move(*roms[0]);
|
||||
rom_.resize(use_zx81_rom ? 8192 : 4096);
|
||||
const ROM::Name rom_name = use_zx81_rom ? ROM::Name::ZX81 : ROM::Name::ZX80;
|
||||
const ROM::Request request(rom_name);
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
rom_ = std::move(roms.find(rom_name)->second);
|
||||
|
||||
rom_mask_ = uint16_t(rom_.size() - 1);
|
||||
|
||||
@@ -472,22 +470,15 @@ template<bool is_zx81> class ConcreteMachine:
|
||||
HalfCycles time_since_ay_update_;
|
||||
inline void ay_set_register(uint8_t value) {
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::BC1);
|
||||
ay_.set_data_input(value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
GI::AY38910::Utility::select_register(ay_, value);
|
||||
}
|
||||
inline void ay_set_data(uint8_t value) {
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR));
|
||||
ay_.set_data_input(value);
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
GI::AY38910::Utility::write_data(ay_, value);
|
||||
}
|
||||
inline uint8_t ay_read_data() {
|
||||
update_audio();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
|
||||
const uint8_t value = ay_.get_data_output();
|
||||
ay_.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
return value;
|
||||
return GI::AY38910::Utility::read(ay_);
|
||||
}
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, time_since_ay_update_.divide_cycles(Cycles(2)));
|
||||
|
||||
@@ -32,7 +32,7 @@ class Machine {
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::QuickloadOption<Options> {
|
||||
friend Configurable::QuickloadOption<Options>;
|
||||
public:
|
||||
bool automatic_tape_motor_control;
|
||||
bool automatic_tape_motor_control = true;
|
||||
|
||||
Options(Configurable::OptionsType type):
|
||||
Configurable::QuickloadOption<Options>(type == Configurable::OptionsType::UserFriendly),
|
||||
|
||||
53
Machines/Sinclair/ZXSpectrum/State.hpp
Normal file
53
Machines/Sinclair/ZXSpectrum/State.hpp
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// State.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 25/04/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef State_hpp
|
||||
#define State_hpp
|
||||
|
||||
#include "../../../Reflection/Struct.hpp"
|
||||
#include "../../../Processors/Z80/State/State.hpp"
|
||||
|
||||
#include "Video.hpp"
|
||||
#include "../../../Components/AY38910/AY38910.hpp"
|
||||
|
||||
namespace Sinclair {
|
||||
namespace ZXSpectrum {
|
||||
|
||||
|
||||
struct State: public Reflection::StructImpl<State> {
|
||||
CPU::Z80::State z80;
|
||||
Video::State video;
|
||||
|
||||
// In 16kb or 48kb mode, RAM will be 16kb or 48kb and represent
|
||||
// memory in standard linear order. In 128kb mode, RAM will be
|
||||
// 128kb with the first 16kb representing bank 0, the next bank 1, etc.
|
||||
std::vector<uint8_t> ram;
|
||||
|
||||
// Meaningful for 128kb machines only.
|
||||
uint8_t last_7ffd = 0;
|
||||
GI::AY38910::State ay;
|
||||
|
||||
// Meaningful for the +2a and +3 only.
|
||||
uint8_t last_1ffd = 0;
|
||||
|
||||
State() {
|
||||
if(needs_declare()) {
|
||||
DeclareField(z80);
|
||||
DeclareField(video);
|
||||
DeclareField(ram);
|
||||
DeclareField(last_7ffd);
|
||||
DeclareField(last_1ffd);
|
||||
DeclareField(ay);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* State_h */
|
||||
@@ -12,13 +12,18 @@
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
#include "../../../Reflection/Struct.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Sinclair {
|
||||
namespace ZXSpectrum {
|
||||
namespace Video {
|
||||
|
||||
enum class VideoTiming {
|
||||
Plus3
|
||||
enum class Timing {
|
||||
FortyEightK,
|
||||
OneTwoEightK,
|
||||
Plus3,
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -45,87 +50,72 @@ enum class VideoTiming {
|
||||
|
||||
*/
|
||||
|
||||
template <VideoTiming timing> class Video {
|
||||
template <Timing timing> class Video {
|
||||
private:
|
||||
struct Timings {
|
||||
// Number of cycles per line. Will be 224 or 228.
|
||||
int cycles_per_line;
|
||||
int half_cycles_per_line;
|
||||
// Number of lines comprising a whole frame. Will be 311 or 312.
|
||||
int lines_per_frame;
|
||||
|
||||
// Number of cycles after first pixel fetch at which interrupt is first signalled.
|
||||
int interrupt_time;
|
||||
|
||||
// Number of cycles before first pixel fetch that contention starts to be applied.
|
||||
int contention_leadin;
|
||||
// Period in a line for which contention is applied.
|
||||
int contention_duration;
|
||||
|
||||
// Contention to apply, in half-cycles, as a function of number of half cycles since
|
||||
// Number of cycles after first pixel fetch at which interrupt is first signalled.
|
||||
int interrupt_time;
|
||||
|
||||
// Contention to apply, in whole cycles, as a function of number of whole cycles since
|
||||
// contention began.
|
||||
int delays[16];
|
||||
int delays[8];
|
||||
|
||||
constexpr Timings(int cycles_per_line, int lines_per_frame, int contention_leadin, int contention_duration, int interrupt_offset, const int *delays) noexcept :
|
||||
half_cycles_per_line(cycles_per_line * 2),
|
||||
lines_per_frame(lines_per_frame),
|
||||
contention_leadin(contention_leadin * 2),
|
||||
contention_duration(contention_duration * 2),
|
||||
interrupt_time((cycles_per_line * lines_per_frame - interrupt_offset - contention_leadin) * 2),
|
||||
delays{ delays[0] * 2, delays[1] * 2, delays[2] * 2, delays[3] * 2, delays[4] * 2, delays[5] * 2, delays[6] * 2, delays[7] * 2}
|
||||
{}
|
||||
};
|
||||
|
||||
static constexpr Timings get_timings() {
|
||||
// Amstrad gate array timings, classic statement:
|
||||
//
|
||||
// Contention begins 14361 cycles "after interrupt" and follows the pattern [1, 0, 7, 6 5 4, 3, 2].
|
||||
// The first four bytes of video are fetched at 14365–14368 cycles, in the order [pixels, attribute, pixels, attribute].
|
||||
//
|
||||
// For my purposes:
|
||||
//
|
||||
// Video fetching always begins at 0. Since there are 311*228 = 70908 cycles per frame, and the interrupt
|
||||
// should "occur" (I assume: begin) 14365 before that, it should actually begin at 70908 - 14365 = 56543.
|
||||
//
|
||||
// Contention begins four cycles before the first video fetch, so it begins at 70904. I don't currently
|
||||
// know whether the four cycles is true across all models, so it's given here as convention_leadin.
|
||||
//
|
||||
// ... except that empirically that all seems to be two cycles off. So maybe I misunderstand what the
|
||||
// contention patterns are supposed to indicate relative to MREQ? It's frustrating that all documentation
|
||||
// I can find is vaguely in terms of contention patterns, and what they mean isn't well-defined in terms
|
||||
// of regular Z80 signalling.
|
||||
constexpr Timings result = {
|
||||
.cycles_per_line = 228 * 2,
|
||||
.lines_per_frame = 311,
|
||||
if constexpr (timing == Timing::Plus3) {
|
||||
constexpr int delays[] = {1, 0, 7, 6, 5, 4, 3, 2};
|
||||
return Timings(228, 311, 6, 129, 14361, delays);
|
||||
}
|
||||
|
||||
.interrupt_time = 56545 * 2,
|
||||
if constexpr (timing == Timing::OneTwoEightK) {
|
||||
constexpr int delays[] = {6, 5, 4, 3, 2, 1, 0, 0};
|
||||
return Timings(228, 311, 4, 128, 14361, delays);
|
||||
}
|
||||
|
||||
.contention_leadin = 2 * 2, // TODO: is this 2? Or 4? Or... ?
|
||||
.contention_duration = 129 * 2,
|
||||
|
||||
.delays = {
|
||||
2, 1,
|
||||
0, 0,
|
||||
14, 13,
|
||||
12, 11,
|
||||
10, 9,
|
||||
8, 7,
|
||||
6, 5,
|
||||
4, 3,
|
||||
}
|
||||
};
|
||||
return result;
|
||||
if constexpr (timing == Timing::FortyEightK) {
|
||||
constexpr int delays[] = {6, 5, 4, 3, 2, 1, 0, 0};
|
||||
return Timings(224, 312, 4, 128, 14335, delays);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: how long is the interrupt line held for?
|
||||
static constexpr int interrupt_duration = 48;
|
||||
// Interrupt should be held for 32 cycles.
|
||||
static constexpr int interrupt_duration = 64;
|
||||
|
||||
public:
|
||||
void run_for(HalfCycles duration) {
|
||||
constexpr auto timings = get_timings();
|
||||
|
||||
constexpr int sync_line = (timings.interrupt_time / timings.cycles_per_line) + 1;
|
||||
constexpr int sync_line = (timings.interrupt_time / timings.half_cycles_per_line) + 1;
|
||||
|
||||
constexpr int sync_position = 166 * 2;
|
||||
constexpr int sync_position = (timing == Timing::FortyEightK) ? 164 * 2 : 166 * 2;
|
||||
constexpr int sync_length = 17 * 2;
|
||||
constexpr int burst_position = sync_position + 40;
|
||||
constexpr int burst_length = 17;
|
||||
|
||||
int cycles_remaining = duration.as<int>();
|
||||
while(cycles_remaining) {
|
||||
int line = time_into_frame_ / timings.cycles_per_line;
|
||||
int offset = time_into_frame_ % timings.cycles_per_line;
|
||||
const int cycles_this_line = std::min(cycles_remaining, timings.cycles_per_line - offset);
|
||||
int line = time_into_frame_ / timings.half_cycles_per_line;
|
||||
int offset = time_into_frame_ % timings.half_cycles_per_line;
|
||||
const int cycles_this_line = std::min(cycles_remaining, timings.half_cycles_per_line - offset);
|
||||
const int end_offset = offset + cycles_this_line;
|
||||
|
||||
if(!offset) {
|
||||
@@ -167,6 +157,8 @@ template <VideoTiming timing> class Video {
|
||||
last_fetches_[1] = memory_[attribute_address_];
|
||||
last_fetches_[2] = memory_[pixel_address_+1];
|
||||
last_fetches_[3] = memory_[attribute_address_+1];
|
||||
set_last_contended_area_access(last_fetches_[3]);
|
||||
|
||||
pixel_address_ += 2;
|
||||
attribute_address_ += 2;
|
||||
|
||||
@@ -230,9 +222,14 @@ template <VideoTiming timing> class Video {
|
||||
|
||||
if(offset >= burst_position && offset < burst_position+burst_length && end_offset > offset) {
|
||||
const int burst_duration = std::min(burst_position + burst_length, end_offset) - offset;
|
||||
crt_.output_colour_burst(burst_duration, 116, is_alternate_line_);
|
||||
|
||||
if constexpr (timing >= Timing::OneTwoEightK) {
|
||||
crt_.output_colour_burst(burst_duration, 116, is_alternate_line_);
|
||||
// The colour burst phase above is an empirical guess. I need to research further.
|
||||
} else {
|
||||
crt_.output_default_colour_burst(burst_duration);
|
||||
}
|
||||
offset += burst_duration;
|
||||
// The colour burst phase above is an empirical guess. I need to research further.
|
||||
}
|
||||
|
||||
if(offset >= burst_position+burst_length && end_offset > offset) {
|
||||
@@ -242,7 +239,7 @@ template <VideoTiming timing> class Video {
|
||||
}
|
||||
|
||||
cycles_remaining -= cycles_this_line;
|
||||
time_into_frame_ = (time_into_frame_ + cycles_this_line) % (timings.cycles_per_line * timings.lines_per_frame);
|
||||
time_into_frame_ = (time_into_frame_ + cycles_this_line) % (timings.half_cycles_per_line * timings.lines_per_frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,14 +250,64 @@ template <VideoTiming timing> class Video {
|
||||
crt_.output_level(duration);
|
||||
}
|
||||
|
||||
static constexpr int half_cycles_per_line() {
|
||||
if constexpr (timing == Timing::FortyEightK) {
|
||||
// TODO: determine real figure here, if one exists.
|
||||
// The source I'm looking at now suggests that the theoretical
|
||||
// ideal of 224*2 ignores the real-life effects of separate
|
||||
// crystals, so I've nudged this experimentally.
|
||||
return 224*2 - 1;
|
||||
} else {
|
||||
return 227*2;
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr HalfCycles frame_duration() {
|
||||
const auto timings = get_timings();
|
||||
return HalfCycles(timings.half_cycles_per_line * timings.lines_per_frame);
|
||||
}
|
||||
|
||||
HalfCycles time_since_interrupt() {
|
||||
const auto timings = get_timings();
|
||||
if(time_into_frame_ >= timings.interrupt_time) {
|
||||
return HalfCycles(time_into_frame_ - timings.interrupt_time);
|
||||
} else {
|
||||
return HalfCycles(time_into_frame_) + frame_duration() - HalfCycles(timings.interrupt_time);
|
||||
}
|
||||
}
|
||||
|
||||
void set_time_since_interrupt(const HalfCycles time) {
|
||||
// Advance using run_for to ensure that all proper CRT interactions occurred.
|
||||
const auto timings = get_timings();
|
||||
const auto target = (time + timings.interrupt_time) % frame_duration();
|
||||
const auto now = HalfCycles(time_into_frame_);
|
||||
|
||||
// Maybe this is easy?
|
||||
if(target == now) return;
|
||||
|
||||
// Is the time within this frame?
|
||||
if(time > now) {
|
||||
run_for(target - time);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then it's necessary to finish this frame and run into the next.
|
||||
run_for(frame_duration() - now + time);
|
||||
}
|
||||
|
||||
public:
|
||||
Video() :
|
||||
crt_(227 * 2, 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2)
|
||||
crt_(half_cycles_per_line(), 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2)
|
||||
{
|
||||
// Show only the centre 80% of the TV frame.
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f));
|
||||
|
||||
// Get the CRT roughly into phase.
|
||||
//
|
||||
// TODO: this is coupled to an assumption about the initial CRT. Fix.
|
||||
const auto timings = get_timings();
|
||||
crt_.output_blank(timings.lines_per_frame*timings.half_cycles_per_line - timings.interrupt_time);
|
||||
}
|
||||
|
||||
void set_video_source(const uint8_t *source) {
|
||||
@@ -284,7 +331,7 @@ template <VideoTiming timing> class Video {
|
||||
}
|
||||
|
||||
// If not, it'll be in the next batch.
|
||||
return timings.interrupt_time + timings.cycles_per_line * timings.lines_per_frame - time_into_frame_;
|
||||
return timings.interrupt_time + timings.half_cycles_per_line * timings.lines_per_frame - time_into_frame_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -299,41 +346,65 @@ template <VideoTiming timing> class Video {
|
||||
@returns How many cycles the [ULA/gate array] would delay the CPU for if it were to recognise that contention
|
||||
needs to be applied in @c offset half-cycles from now.
|
||||
*/
|
||||
int access_delay(HalfCycles offset) const {
|
||||
HalfCycles access_delay(HalfCycles offset) const {
|
||||
constexpr auto timings = get_timings();
|
||||
const int delay_time = (time_into_frame_ + offset.as<int>() + timings.contention_leadin) % (timings.cycles_per_line * timings.lines_per_frame);
|
||||
const int delay_time = (time_into_frame_ + offset.as<int>() + timings.contention_leadin) % (timings.half_cycles_per_line * timings.lines_per_frame);
|
||||
assert(!(delay_time&1));
|
||||
|
||||
// Check for a time within the no-contention window.
|
||||
if(delay_time >= (191*timings.cycles_per_line + timings.contention_duration)) {
|
||||
if(delay_time >= (191*timings.half_cycles_per_line + timings.contention_duration)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int time_into_line = delay_time % timings.cycles_per_line;
|
||||
if(time_into_line >= timings.contention_duration) return 0;
|
||||
const int time_into_line = delay_time % timings.half_cycles_per_line;
|
||||
if(time_into_line >= timings.contention_duration) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return timings.delays[time_into_line & 15];
|
||||
return HalfCycles(timings.delays[(time_into_line >> 1) & 7]);
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns Whatever the ULA or gate array has fetched this cycle, or 0xff if it has fetched nothing.
|
||||
@returns Whatever the ULA or gate array would expose via the floating bus, this cycle.
|
||||
*/
|
||||
uint8_t get_current_fetch() const {
|
||||
uint8_t get_floating_value() const {
|
||||
constexpr auto timings = get_timings();
|
||||
const int line = time_into_frame_ / timings.cycles_per_line;
|
||||
if(line >= 192) return 0xff;
|
||||
const uint8_t out_of_bounds = (timing == Timing::Plus3) ? last_contended_access_ : 0xff;
|
||||
|
||||
const int time_into_line = time_into_frame_ % timings.cycles_per_line;
|
||||
if(time_into_line >= 256 || (time_into_line&8)) {
|
||||
return 0xff;
|
||||
const int line = time_into_frame_ / timings.half_cycles_per_line;
|
||||
if(line >= 192) {
|
||||
return out_of_bounds;
|
||||
}
|
||||
|
||||
return last_fetches_[(time_into_line >> 1) & 3];
|
||||
const int time_into_line = time_into_frame_ % timings.half_cycles_per_line;
|
||||
if(time_into_line >= 256 || (time_into_line&8)) {
|
||||
return out_of_bounds;
|
||||
}
|
||||
|
||||
// The +2a and +3 always return the low bit as set.
|
||||
const uint8_t value = last_fetches_[(time_into_line >> 1) & 3];
|
||||
if constexpr (timing == Timing::Plus3) {
|
||||
return value | 1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/*!
|
||||
Relevant to the +2a and +3 only, sets the most recent value read from or
|
||||
written to contended memory. This is what will be returned if the floating
|
||||
bus is accessed when the gate array isn't currently reading.
|
||||
*/
|
||||
void set_last_contended_area_access([[maybe_unused]] uint8_t value) {
|
||||
if constexpr (timing == Timing::Plus3) {
|
||||
last_contended_access_ = value | 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current border colour.
|
||||
*/
|
||||
void set_border_colour(uint8_t colour) {
|
||||
border_byte_ = colour;
|
||||
border_colour_ = palette[colour];
|
||||
}
|
||||
|
||||
@@ -352,11 +423,17 @@ template <VideoTiming timing> class Video {
|
||||
crt_.set_display_type(type);
|
||||
}
|
||||
|
||||
/*! Gets the display type. */
|
||||
Outputs::Display::DisplayType get_display_type() const {
|
||||
return crt_.get_display_type();
|
||||
}
|
||||
|
||||
private:
|
||||
int time_into_frame_ = 0;
|
||||
Outputs::CRT::CRT crt_;
|
||||
const uint8_t *memory_ = nullptr;
|
||||
uint8_t border_colour_ = 0;
|
||||
uint8_t border_byte_ = 0;
|
||||
|
||||
uint8_t *pixel_target_ = nullptr;
|
||||
int attribute_address_ = 0;
|
||||
@@ -367,6 +444,9 @@ template <VideoTiming timing> class Video {
|
||||
bool is_alternate_line_ = false;
|
||||
|
||||
uint8_t last_fetches_[4] = {0xff, 0xff, 0xff, 0xff};
|
||||
uint8_t last_contended_access_ = 0xff;
|
||||
|
||||
friend struct State;
|
||||
|
||||
#define RGB(r, g, b) (r << 4) | (g << 2) | b
|
||||
static constexpr uint8_t palette[] = {
|
||||
@@ -378,6 +458,41 @@ template <VideoTiming timing> class Video {
|
||||
#undef RGB
|
||||
};
|
||||
|
||||
struct State: public Reflection::StructImpl<State> {
|
||||
uint8_t border_colour = 0;
|
||||
int half_cycles_since_interrupt = 0;
|
||||
bool flash = 0;
|
||||
int flash_counter = 0;
|
||||
bool is_alternate_line = false;
|
||||
|
||||
State() {
|
||||
if(needs_declare()) {
|
||||
DeclareField(border_colour);
|
||||
DeclareField(half_cycles_since_interrupt);
|
||||
DeclareField(flash);
|
||||
DeclareField(flash_counter);
|
||||
DeclareField(is_alternate_line);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Video> State(const Video &source) : State() {
|
||||
border_colour = source.border_byte_;
|
||||
flash = source.flash_mask_;
|
||||
flash_counter = source.flash_counter_;
|
||||
is_alternate_line = source. is_alternate_line_;
|
||||
half_cycles_since_interrupt = source.time_since_interrupt().template as<int>();
|
||||
}
|
||||
|
||||
template <typename Video> void apply(Video &target) {
|
||||
target.set_border_colour(border_colour);
|
||||
target.flash_mask_ = flash ? 0xff : 0x00;
|
||||
target.flash_counter_ = flash_counter;
|
||||
target.is_alternate_line_ = is_alternate_line;
|
||||
target.set_time_since_interrupt(HalfCycles(half_cycles_since_interrupt));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
#include "ZXSpectrum.hpp"
|
||||
|
||||
#include "State.hpp"
|
||||
#include "Video.hpp"
|
||||
|
||||
#define LOG_PREFIX "[Spectrum] "
|
||||
#include "../Keyboard/Keyboard.hpp"
|
||||
|
||||
#include "../../../Activity/Source.hpp"
|
||||
#include "../../MachineTypes.hpp"
|
||||
@@ -24,7 +24,9 @@
|
||||
// just grab the CPC's version of an FDC.
|
||||
#include "../../AmstradCPC/FDC.hpp"
|
||||
|
||||
#define LOG_PREFIX "[Spectrum] "
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
#include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp"
|
||||
@@ -39,10 +41,76 @@
|
||||
|
||||
#include "../../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include "../Keyboard/Keyboard.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace {
|
||||
|
||||
/*!
|
||||
Provides a simultaneous Kempston and Interface 2-style joystick.
|
||||
*/
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Up),
|
||||
Input(Input::Down),
|
||||
Input(Input::Left),
|
||||
Input(Input::Right),
|
||||
Input(Input::Fire)
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
#define APPLY_KEMPSTON(b) if(is_active) kempston_ |= b; else kempston_ &= ~b;
|
||||
#define APPLY_SINCLAIR(b) if(is_active) sinclair_ &= ~b; else sinclair_ |= b;
|
||||
|
||||
switch(digital_input.type) {
|
||||
default: return;
|
||||
|
||||
case Input::Right:
|
||||
APPLY_KEMPSTON(0x01);
|
||||
APPLY_SINCLAIR(0x0208);
|
||||
break;
|
||||
case Input::Left:
|
||||
APPLY_KEMPSTON(0x02);
|
||||
APPLY_SINCLAIR(0x0110);
|
||||
break;
|
||||
case Input::Down:
|
||||
APPLY_KEMPSTON(0x04);
|
||||
APPLY_SINCLAIR(0x0404);
|
||||
break;
|
||||
case Input::Up:
|
||||
APPLY_KEMPSTON(0x08);
|
||||
APPLY_SINCLAIR(0x0802);
|
||||
break;
|
||||
case Input::Fire:
|
||||
APPLY_KEMPSTON(0x10);
|
||||
APPLY_SINCLAIR(0x1001);
|
||||
break;
|
||||
}
|
||||
|
||||
#undef APPLY_KEMPSTON
|
||||
#undef APPLY_SINCLAIR
|
||||
}
|
||||
|
||||
/// @returns The value that a Kempston joystick interface would report if this joystick
|
||||
/// were plugged into it.
|
||||
uint8_t get_kempston() {
|
||||
return kempston_;
|
||||
}
|
||||
|
||||
/// @returns The value that a Sinclair interface would report if this joystick
|
||||
/// were plugged into it via @c port (which should be either 0 or 1, for ports 1 or 2).
|
||||
uint8_t get_sinclair(int port) {
|
||||
return uint8_t(sinclair_ >> (port * 8));
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t kempston_ = 0x00;
|
||||
uint16_t sinclair_ = 0xffff;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace Sinclair {
|
||||
namespace ZXSpectrum {
|
||||
|
||||
@@ -56,6 +124,7 @@ template<Model model> class ConcreteMachine:
|
||||
public CPU::Z80::BusHandler,
|
||||
public Machine,
|
||||
public MachineTypes::AudioProducer,
|
||||
public MachineTypes::JoystickMachine,
|
||||
public MachineTypes::MappedKeyboardMachine,
|
||||
public MachineTypes::MediaTarget,
|
||||
public MachineTypes::ScanProducer,
|
||||
@@ -77,17 +146,32 @@ template<Model model> class ConcreteMachine:
|
||||
set_clock_rate(clock_rate());
|
||||
speaker_.set_input_rate(float(clock_rate()) / 2.0f);
|
||||
|
||||
// With only the +2a and +3 currently supported, the +3 ROM is always
|
||||
// the one required.
|
||||
const auto roms =
|
||||
rom_fetcher({ {"ZXSpectrum", "the +2a/+3 ROM", "plus3.rom", 64 * 1024, 0x96e3c17a} });
|
||||
if(!roms[0]) throw ROMMachine::Error::MissingROMs;
|
||||
memcpy(rom_.data(), roms[0]->data(), std::min(rom_.size(), roms[0]->size()));
|
||||
ROM::Name rom_name;
|
||||
switch(model) {
|
||||
case Model::SixteenK:
|
||||
case Model::FortyEightK: rom_name = ROM::Name::Spectrum48k; break;
|
||||
case Model::OneTwoEightK: rom_name = ROM::Name::Spectrum128k; break;
|
||||
case Model::Plus2: rom_name = ROM::Name::SpecrumPlus2; break;
|
||||
case Model::Plus2a:
|
||||
case Model::Plus3: rom_name = ROM::Name::SpectrumPlus3; break;
|
||||
// TODO: possibly accept the +3 ROM in multiple parts?
|
||||
}
|
||||
const auto request = ROM::Request(rom_name);
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
const auto &rom = roms.find(rom_name)->second;
|
||||
memcpy(rom_.data(), rom.data(), std::min(rom_.size(), rom.size()));
|
||||
|
||||
// Register for sleeping notifications.
|
||||
fdc_->set_clocking_hint_observer(this);
|
||||
tape_player_.set_clocking_hint_observer(this);
|
||||
|
||||
// Attach a couple of joysticks.
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
|
||||
// Set up initial memory map.
|
||||
update_memory_map();
|
||||
set_video_address();
|
||||
@@ -102,6 +186,29 @@ template<Model model> class ConcreteMachine:
|
||||
duration_to_press_enter_ = Cycles(5 * clock_rate());
|
||||
keyboard_.set_key_state(ZX::Keyboard::KeyEnter, true);
|
||||
}
|
||||
|
||||
// Install state if supplied.
|
||||
if(target.state) {
|
||||
const auto state = static_cast<State *>(target.state.get());
|
||||
state->z80.apply(z80_);
|
||||
state->video.apply(*video_.last_valid());
|
||||
state->ay.apply(ay_);
|
||||
|
||||
// If this is a 48k or 16k machine, remap source data from its original
|
||||
// linear form to whatever the banks end up being; otherwise copy as is.
|
||||
if(model <= Model::FortyEightK) {
|
||||
const size_t num_banks = std::min(size_t(48*1024), state->ram.size()) >> 14;
|
||||
for(size_t c = 0; c < num_banks; c++) {
|
||||
memcpy(&write_pointers_[c + 1][(c+1) * 0x4000], &state->ram[c * 0x4000], 0x4000);
|
||||
}
|
||||
} else {
|
||||
memcpy(ram_.data(), state->ram.data(), std::min(ram_.size(), state->ram.size()));
|
||||
|
||||
port1ffd_ = state->last_1ffd;
|
||||
port7ffd_ = state->last_7ffd;
|
||||
update_memory_map();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
@@ -109,7 +216,7 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
static constexpr unsigned int clock_rate() {
|
||||
// constexpr unsigned int ClockRate = 3'500'000;
|
||||
constexpr unsigned int OriginalClockRate = 3'500'000;
|
||||
constexpr unsigned int Plus3ClockRate = 3'546'875; // See notes below; this is a guess.
|
||||
|
||||
// Notes on timing for the +2a and +3:
|
||||
@@ -136,7 +243,7 @@ template<Model model> class ConcreteMachine:
|
||||
// the Spectrum is a PAL machine with a fixed colour phase relationship. For
|
||||
// this emulator's world, that's a first!
|
||||
|
||||
return Plus3ClockRate;
|
||||
return model < Model::OneTwoEightK ? OriginalClockRate : Plus3ClockRate;
|
||||
}
|
||||
|
||||
// MARK: - TimedMachine.
|
||||
@@ -180,49 +287,149 @@ template<Model model> class ConcreteMachine:
|
||||
video_->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType get_display_type() const override {
|
||||
return video_->get_display_type();
|
||||
}
|
||||
|
||||
// MARK: - BusHandler.
|
||||
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
using PartialMachineCycle = CPU::Z80::PartialMachineCycle;
|
||||
|
||||
HalfCycles delay(0);
|
||||
const uint16_t address = cycle.address ? *cycle.address : 0x0000;
|
||||
|
||||
// Apply contention if necessary.
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
// Model applied: the trigger for the ULA inserting a delay is the falling edge
|
||||
// of MREQ, which is always half a cycle into a read or write.
|
||||
if(
|
||||
is_contended_[address >> 14] &&
|
||||
cycle.operation >= PartialMachineCycle::ReadOpcodeStart &&
|
||||
cycle.operation <= PartialMachineCycle::WriteStart) {
|
||||
|
||||
const auto delay = video_.last_valid()->access_delay(video_.time_since_flush());
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
}
|
||||
} else {
|
||||
switch(cycle.operation) {
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
case CPU::Z80::PartialMachineCycle::Output:
|
||||
case CPU::Z80::PartialMachineCycle::Read:
|
||||
case CPU::Z80::PartialMachineCycle::Write:
|
||||
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
||||
case CPU::Z80::PartialMachineCycle::Interrupt:
|
||||
// For these, carry on into the actual handler, below.
|
||||
break;
|
||||
|
||||
// For anything else that isn't listed below, just advance
|
||||
// time and conclude here.
|
||||
default:
|
||||
advance(cycle.length);
|
||||
return HalfCycles(0);
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::InputStart:
|
||||
case CPU::Z80::PartialMachineCycle::OutputStart: {
|
||||
// The port address is loaded prior to IOREQ being visible; a contention
|
||||
// always occurs if it is in the $4000–$8000 range regardless of current
|
||||
// memory mapping.
|
||||
HalfCycles delay;
|
||||
HalfCycles time = video_.time_since_flush();
|
||||
|
||||
if((address & 0xc000) == 0x4000) {
|
||||
for(int c = 0; c < ((address & 1) ? 4 : 2); c++) {
|
||||
const auto next_delay = video_.last_valid()->access_delay(time);
|
||||
delay += next_delay;
|
||||
time += next_delay + 2;
|
||||
}
|
||||
} else {
|
||||
if(!(address & 1)) {
|
||||
delay = video_.last_valid()->access_delay(time + HalfCycles(2));
|
||||
}
|
||||
}
|
||||
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
} break;
|
||||
|
||||
case PartialMachineCycle::ReadOpcodeStart:
|
||||
case PartialMachineCycle::ReadStart:
|
||||
case PartialMachineCycle::WriteStart: {
|
||||
// These all start by loading the address bus, then set MREQ
|
||||
// half a cycle later.
|
||||
if(is_contended_[address >> 14]) {
|
||||
const auto delay = video_.last_valid()->access_delay(video_.time_since_flush());
|
||||
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
}
|
||||
} break;
|
||||
|
||||
case PartialMachineCycle::Internal: {
|
||||
// Whatever's on the address bus will remain there, without IOREQ or
|
||||
// MREQ interceding, for this entire bus cycle. So apply contentions
|
||||
// all the way along.
|
||||
if(is_contended_[address >> 14]) {
|
||||
const auto half_cycles = cycle.length.as<int>();
|
||||
assert(!(half_cycles & 1));
|
||||
|
||||
HalfCycles time = video_.time_since_flush();
|
||||
HalfCycles delay;
|
||||
for(int c = 0; c < half_cycles; c += 2) {
|
||||
const auto next_delay = video_.last_valid()->access_delay(time);
|
||||
delay += next_delay;
|
||||
time += next_delay + 2;
|
||||
}
|
||||
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// For all other machine cycles, model the action as happening at the end of the machine cycle;
|
||||
// that means advancing time now.
|
||||
advance(cycle.length);
|
||||
|
||||
switch(cycle.operation) {
|
||||
default: break;
|
||||
|
||||
case PartialMachineCycle::ReadOpcodeStart:
|
||||
case PartialMachineCycle::ReadStart:
|
||||
case PartialMachineCycle::WriteStart:
|
||||
// Apply contention if necessary.
|
||||
//
|
||||
// Assumption here: the trigger for the ULA inserting a delay is the falling edge
|
||||
// of MREQ, which is always half a cycle into a read or write.
|
||||
//
|
||||
// TODO: somehow provide that information in the PartialMachineCycle?
|
||||
if(is_contended_[address >> 14]) {
|
||||
delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1));
|
||||
}
|
||||
break;
|
||||
|
||||
case PartialMachineCycle::ReadOpcode:
|
||||
// Fast loading: ROM version.
|
||||
//
|
||||
// The below patches over the 'LD-BYTES' routine from the 48kb ROM.
|
||||
if(use_fast_tape_hack_ && address == 0x0556 && read_pointers_[0] == &rom_[0xc000]) {
|
||||
if(perform_rom_ld_bytes()) {
|
||||
// Stop pressing enter, if neccessry.
|
||||
if(duration_to_press_enter_ > Cycles(0)) {
|
||||
duration_to_press_enter_ = Cycles(0);
|
||||
keyboard_.set_key_state(ZX::Keyboard::KeyEnter, false);
|
||||
}
|
||||
// The below patches over part of the 'LD-BYTES' routine from the 48kb ROM.
|
||||
if(use_fast_tape_hack_ && address == 0x056b && read_pointers_[0] == &rom_[classic_rom_offset()]) {
|
||||
// Stop pressing enter, if neccessry.
|
||||
if(duration_to_press_enter_ > Cycles(0)) {
|
||||
duration_to_press_enter_ = Cycles(0);
|
||||
keyboard_.set_key_state(ZX::Keyboard::KeyEnter, false);
|
||||
}
|
||||
|
||||
if(perform_rom_ld_bytes_56b()) {
|
||||
*cycle.value = 0xc9; // i.e. RET.
|
||||
break;
|
||||
}
|
||||
}
|
||||
[[fallthrough]];
|
||||
|
||||
case PartialMachineCycle::Read:
|
||||
if constexpr (model == Model::SixteenK) {
|
||||
// Assumption: with nothing mapped above 0x8000 on the 16kb Spectrum,
|
||||
// read the floating bus.
|
||||
if(address >= 0x8000) {
|
||||
*cycle.value = video_->get_floating_value();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
*cycle.value = read_pointers_[address >> 14][address];
|
||||
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
if(is_contended_[address >> 14]) {
|
||||
video_->set_last_contended_area_access(*cycle.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PartialMachineCycle::Write:
|
||||
@@ -230,9 +437,20 @@ template<Model model> class ConcreteMachine:
|
||||
if(is_video_[address >> 14] && (address & 0x3fff) < 6912) {
|
||||
video_.flush();
|
||||
}
|
||||
|
||||
write_pointers_[address >> 14][address] = *cycle.value;
|
||||
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
// Fill the floating bus buffer if this write is within the contended area.
|
||||
if(is_contended_[address >> 14]) {
|
||||
video_->set_last_contended_area_access(*cycle.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Partial port decodings here and in ::Input are as documented
|
||||
// at https://worldofspectrum.org/faq/reference/ports.htm
|
||||
|
||||
case PartialMachineCycle::Output:
|
||||
// Test for port FE.
|
||||
if(!(address&1)) {
|
||||
@@ -247,55 +465,70 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// Test for classic 128kb paging register (i.e. port 7ffd).
|
||||
if((address & 0xc002) == 0x4000) {
|
||||
if (
|
||||
(model >= Model::OneTwoEightK && model <= Model::Plus2 && (address & 0x8002) == 0x0000) ||
|
||||
(model >= Model::Plus2a && (address & 0xc002) == 0x4000)
|
||||
) {
|
||||
port7ffd_ = *cycle.value;
|
||||
update_memory_map();
|
||||
|
||||
// Set the proper video base pointer.
|
||||
set_video_address();
|
||||
|
||||
// Potentially lock paging, _after_ the current
|
||||
// port values have taken effect.
|
||||
disable_paging_ |= *cycle.value & 0x20;
|
||||
}
|
||||
|
||||
// Test for +2a/+3 paging (i.e. port 1ffd).
|
||||
if((address & 0xf002) == 0x1000) {
|
||||
port1ffd_ = *cycle.value;
|
||||
update_memory_map();
|
||||
update_video_base();
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
if((address & 0xf002) == 0x1000) {
|
||||
port1ffd_ = *cycle.value;
|
||||
update_memory_map();
|
||||
update_video_base();
|
||||
|
||||
if constexpr (model == Model::Plus3) {
|
||||
fdc_->set_motor_on(*cycle.value & 0x08);
|
||||
if constexpr (model == Model::Plus3) {
|
||||
fdc_->set_motor_on(*cycle.value & 0x08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if((address & 0xc002) == 0xc000) {
|
||||
// Select AY register.
|
||||
update_audio();
|
||||
GI::AY38910::Utility::select_register(ay_, *cycle.value);
|
||||
}
|
||||
|
||||
if((address & 0xc002) == 0x8000) {
|
||||
// Write to AY register.
|
||||
update_audio();
|
||||
GI::AY38910::Utility::write_data(ay_, *cycle.value);
|
||||
// Route to the AY if one is fitted.
|
||||
if constexpr (model >= Model::OneTwoEightK) {
|
||||
switch(address & 0xc002) {
|
||||
case 0xc000:
|
||||
// Select AY register.
|
||||
update_audio();
|
||||
GI::AY38910::Utility::select_register(ay_, *cycle.value);
|
||||
break;
|
||||
|
||||
case 0x8000:
|
||||
// Write to AY register.
|
||||
update_audio();
|
||||
GI::AY38910::Utility::write_data(ay_, *cycle.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for FDC accesses.
|
||||
if constexpr (model == Model::Plus3) {
|
||||
switch(address) {
|
||||
switch(address & 0xf002) {
|
||||
default: break;
|
||||
case 0x3ffd: case 0x2ffd:
|
||||
case 0x3000: case 0x2000:
|
||||
fdc_->write((address >> 12) & 1, *cycle.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PartialMachineCycle::Input:
|
||||
case PartialMachineCycle::Input: {
|
||||
bool did_match = false;
|
||||
*cycle.value = 0xff;
|
||||
|
||||
if(!(address&32)) {
|
||||
did_match = true;
|
||||
*cycle.value &= static_cast<Joystick *>(joysticks_[0].get())->get_kempston();
|
||||
}
|
||||
|
||||
if(!(address&1)) {
|
||||
did_match = true;
|
||||
|
||||
// Port FE:
|
||||
//
|
||||
// address b8+: mask of keyboard lines to select
|
||||
@@ -305,11 +538,15 @@ template<Model model> class ConcreteMachine:
|
||||
*cycle.value &= keyboard_.read(address);
|
||||
*cycle.value &= tape_player_.get_input() ? 0xbf : 0xff;
|
||||
|
||||
// If this read is within 200 cycles of the previous,
|
||||
// count it as an adjacent hit; if 20 of those have
|
||||
// occurred then start the tape motor.
|
||||
// Add Joystick input on top.
|
||||
if(!(address&0x1000)) *cycle.value &= static_cast<Joystick *>(joysticks_[0].get())->get_sinclair(0);
|
||||
if(!(address&0x0800)) *cycle.value &= static_cast<Joystick *>(joysticks_[1].get())->get_sinclair(1);
|
||||
|
||||
// If this read is between 50 and 200 cycles since the
|
||||
// previous, count it as an adjacent hit; if 20 of those
|
||||
// have occurred then start the tape motor.
|
||||
if(use_automatic_tape_motor_control_) {
|
||||
if(cycles_since_tape_input_read_ < HalfCycles(400)) {
|
||||
if(cycles_since_tape_input_read_ >= HalfCycles(100) && cycles_since_tape_input_read_ < HalfCycles(200)) {
|
||||
++recent_tape_hits_;
|
||||
|
||||
if(recent_tape_hits_ == 20) {
|
||||
@@ -323,31 +560,50 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
if((address & 0xc002) == 0xc000) {
|
||||
// Read from AY register.
|
||||
update_audio();
|
||||
*cycle.value &= GI::AY38910::Utility::read_data(ay_);
|
||||
if constexpr (model >= Model::OneTwoEightK) {
|
||||
if((address & 0xc002) == 0xc000) {
|
||||
did_match = true;
|
||||
|
||||
// Read from AY register.
|
||||
update_audio();
|
||||
*cycle.value &= GI::AY38910::Utility::read(ay_);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a floating bus read; these are particularly arcane
|
||||
// on the +2a/+3. See footnote to https://spectrumforeveryone.com/technical/memory-contention-floating-bus/
|
||||
if((address & 0xf003) == 0x0001) {
|
||||
*cycle.value &= video_->get_current_fetch();
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
// Check for a +2a/+3 floating bus read; these are particularly arcane.
|
||||
// See footnote to https://spectrumforeveryone.com/technical/memory-contention-floating-bus/
|
||||
// and, much more rigorously, http://sky.relative-path.com/zx/floating_bus.html
|
||||
if(!disable_paging_ && (address & 0xf003) == 0x0001) {
|
||||
*cycle.value &= video_->get_floating_value();
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (model == Model::Plus3) {
|
||||
switch(address) {
|
||||
switch(address & 0xf002) {
|
||||
default: break;
|
||||
case 0x3ffd: case 0x2ffd:
|
||||
case 0x3000: case 0x2000:
|
||||
*cycle.value &= fdc_->read((address >> 12) & 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (model <= Model::Plus2) {
|
||||
if(!did_match) {
|
||||
*cycle.value = video_->get_floating_value();
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case PartialMachineCycle::Interrupt:
|
||||
// At least one piece of Spectrum software, Escape from M.O.N.J.A.S. explicitly
|
||||
// assumes that a 0xff value will be on the bus during an interrupt acknowledgment.
|
||||
// I wasn't otherwise aware that this value is reliable.
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
}
|
||||
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -356,24 +612,24 @@ template<Model model> class ConcreteMachine:
|
||||
|
||||
video_ += duration;
|
||||
if(video_.did_flush()) {
|
||||
z80_.set_interrupt_line(video_.last_valid()->get_interrupt_line());
|
||||
z80_.set_interrupt_line(video_.last_valid()->get_interrupt_line(), video_.last_sequence_point_overrun());
|
||||
}
|
||||
|
||||
if(!tape_player_is_sleeping_) tape_player_.run_for(duration.as_integral());
|
||||
|
||||
// Update automatic tape motor control, if enabled; if it's been
|
||||
// 3 seconds since software last possibly polled the tape, stop it.
|
||||
if(use_automatic_tape_motor_control_ && cycles_since_tape_input_read_ < HalfCycles(clock_rate() * 6)) {
|
||||
// 0.5 seconds since software last possibly polled the tape, stop it.
|
||||
if(use_automatic_tape_motor_control_ && cycles_since_tape_input_read_ < HalfCycles(clock_rate())) {
|
||||
cycles_since_tape_input_read_ += duration;
|
||||
|
||||
if(cycles_since_tape_input_read_ >= HalfCycles(clock_rate() * 6)) {
|
||||
if(cycles_since_tape_input_read_ >= HalfCycles(clock_rate())) {
|
||||
tape_player_.set_motor_control(false);
|
||||
recent_tape_hits_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (model == Model::Plus3) {
|
||||
if(!fdc_is_sleeping_) fdc_ += Cycles(duration.as_integral());
|
||||
fdc_ += Cycles(duration.as_integral());
|
||||
}
|
||||
|
||||
if(typer_) typer_->run_for(duration);
|
||||
@@ -421,6 +677,7 @@ template<Model model> class ConcreteMachine:
|
||||
// If there are any tapes supplied, use the first of them.
|
||||
if(!media.tapes.empty()) {
|
||||
tape_player_.set_tape(media.tapes.front());
|
||||
set_use_fast_tape();
|
||||
}
|
||||
|
||||
// Insert up to four disks.
|
||||
@@ -437,7 +694,6 @@ template<Model model> class ConcreteMachine:
|
||||
// MARK: - ClockingHint::Observer.
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) override {
|
||||
fdc_is_sleeping_ = fdc_.last_valid()->preferred_clocking() == ClockingHint::Preference::None;
|
||||
tape_player_is_sleeping_ = tape_player_.preferred_clocking() == ClockingHint::Preference::None;
|
||||
}
|
||||
|
||||
@@ -464,6 +720,7 @@ template<Model model> class ConcreteMachine:
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly); // OptionsType is arbitrary, but not optional.
|
||||
options->automatic_tape_motor_control = use_automatic_tape_motor_control_;
|
||||
options->quickload = allow_fast_tape_hack_;
|
||||
options->output = get_video_signal_configurable();
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -552,13 +809,21 @@ template<Model model> class ConcreteMachine:
|
||||
set_memory(2, 2);
|
||||
set_memory(3, port7ffd_ & 7);
|
||||
}
|
||||
|
||||
// Potentially lock paging, _after_ the current
|
||||
// port values have taken effect.
|
||||
disable_paging_ = port7ffd_ & 0x20;
|
||||
}
|
||||
|
||||
void set_memory(int bank, uint8_t source) {
|
||||
is_contended_[bank] = (source >= 4 && source < 8);
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
is_contended_[bank] = source >= 4 && source < 8;
|
||||
} else {
|
||||
is_contended_[bank] = source < 0x80 && source & 1;
|
||||
}
|
||||
pages_[bank] = source;
|
||||
|
||||
uint8_t *read = (source < 0x80) ? &ram_[source * 16384] : &rom_[(source & 0x7f) * 16384];
|
||||
uint8_t *const read = (source < 0x80) ? &ram_[source * 16384] : &rom_[(source & 0x7f) * 16384];
|
||||
const auto offset = bank*16384;
|
||||
|
||||
read_pointers_[bank] = read - offset;
|
||||
@@ -591,8 +856,15 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: - Video.
|
||||
static constexpr VideoTiming video_timing = VideoTiming::Plus3;
|
||||
JustInTimeActor<Video<video_timing>> video_;
|
||||
using VideoType =
|
||||
std::conditional_t<
|
||||
model <= Model::FortyEightK, Video::Video<Video::Timing::FortyEightK>,
|
||||
std::conditional_t<
|
||||
model <= Model::Plus2, Video::Video<Video::Timing::OneTwoEightK>,
|
||||
Video::Video<Video::Timing::Plus3>
|
||||
>
|
||||
>;
|
||||
JustInTimeActor<VideoType> video_;
|
||||
|
||||
// MARK: - Keyboard.
|
||||
Sinclair::ZX::Keyboard::Keyboard keyboard_;
|
||||
@@ -613,31 +885,42 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// Reimplements the 'LD-BYTES' routine, as documented at
|
||||
// https://skoolkid.github.io/rom/asm/0556.html i.e.
|
||||
// https://skoolkid.github.io/rom/asm/0556.html but picking
|
||||
// up from address 56b i.e.
|
||||
//
|
||||
// In:
|
||||
// A: 0x00 or 0xff for block type;
|
||||
// F: carry set if loading, clear if verifying;
|
||||
// A': 0x00 or 0xff for block type;
|
||||
// F': carry set if loading, clear if verifying;
|
||||
// DE: block length;
|
||||
// IX: start address.
|
||||
//
|
||||
// Out:
|
||||
// F: carry set for success, clear for error.
|
||||
bool perform_rom_ld_bytes() {
|
||||
//
|
||||
// And, empirically:
|
||||
// IX: one beyond final address written;
|
||||
// DE: 0;
|
||||
// L: parity byte;
|
||||
// H: 0 for no error, 0xff for error;
|
||||
// A: same as H.
|
||||
// BC: ???
|
||||
bool perform_rom_ld_bytes_56b() {
|
||||
using Parser = Storage::Tape::ZXSpectrum::Parser;
|
||||
Parser parser(Parser::MachineType::ZXSpectrum);
|
||||
|
||||
using Register = CPU::Z80::Register;
|
||||
uint8_t flags = uint8_t(z80_.get_value_of_register(Register::Flags));
|
||||
uint8_t flags = uint8_t(z80_.get_value_of_register(Register::FlagsDash));
|
||||
if(!(flags & 1)) return false;
|
||||
|
||||
const uint8_t block_type = uint8_t(z80_.get_value_of_register(Register::A));
|
||||
const uint8_t block_type = uint8_t(z80_.get_value_of_register(Register::ADash));
|
||||
const auto block = parser.find_block(tape_player_.get_tape());
|
||||
if(!block || block_type != (*block).type) return false;
|
||||
|
||||
uint16_t length = z80_.get_value_of_register(Register::DE);
|
||||
uint16_t target = z80_.get_value_of_register(Register::IX);
|
||||
|
||||
flags = 0x93;
|
||||
uint8_t parity = 0x00;
|
||||
while(length--) {
|
||||
auto next = parser.get_byte(tape_player_.get_tape());
|
||||
if(!next) {
|
||||
@@ -646,19 +929,55 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
write_pointers_[target >> 14][target] = *next;
|
||||
parity ^= *next;
|
||||
++target;
|
||||
}
|
||||
|
||||
auto stored_parity = parser.get_byte(tape_player_.get_tape());
|
||||
if(!stored_parity) {
|
||||
flags &= ~1;
|
||||
} else {
|
||||
z80_.set_value_of_register(Register::L, *stored_parity);
|
||||
}
|
||||
|
||||
z80_.set_value_of_register(Register::Flags, flags);
|
||||
z80_.set_value_of_register(Register::DE, length);
|
||||
z80_.set_value_of_register(Register::IX, target);
|
||||
|
||||
const uint8_t h = (flags & 1) ? 0x00 : 0xff;
|
||||
z80_.set_value_of_register(Register::H, h);
|
||||
z80_.set_value_of_register(Register::A, h);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static constexpr int classic_rom_offset() {
|
||||
switch(model) {
|
||||
case Model::SixteenK:
|
||||
case Model::FortyEightK:
|
||||
return 0x0000;
|
||||
|
||||
case Model::OneTwoEightK:
|
||||
case Model::Plus2:
|
||||
return 0x4000;
|
||||
|
||||
case Model::Plus2a:
|
||||
case Model::Plus3:
|
||||
return 0xc000;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Disc.
|
||||
JustInTimeActor<Amstrad::FDC, 1, 1, Cycles> fdc_;
|
||||
bool fdc_is_sleeping_ = false;
|
||||
JustInTimeActor<Amstrad::FDC, Cycles> fdc_;
|
||||
|
||||
// MARK: - Automatic startup.
|
||||
Cycles duration_to_press_enter_;
|
||||
|
||||
// MARK: - Joysticks
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
return joysticks_;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -671,8 +990,12 @@ Machine *Machine::ZXSpectrum(const Analyser::Static::Target *target, const ROMMa
|
||||
const auto zx_target = dynamic_cast<const Analyser::Static::ZXSpectrum::Target *>(target);
|
||||
|
||||
switch(zx_target->model) {
|
||||
case Model::Plus2a: return new ConcreteMachine<Model::Plus2a>(*zx_target, rom_fetcher);
|
||||
case Model::Plus3: return new ConcreteMachine<Model::Plus3>(*zx_target, rom_fetcher);
|
||||
case Model::SixteenK: return new ConcreteMachine<Model::SixteenK>(*zx_target, rom_fetcher);
|
||||
case Model::FortyEightK: return new ConcreteMachine<Model::FortyEightK>(*zx_target, rom_fetcher);
|
||||
case Model::OneTwoEightK: return new ConcreteMachine<Model::OneTwoEightK>(*zx_target, rom_fetcher);
|
||||
case Model::Plus2: return new ConcreteMachine<Model::Plus2>(*zx_target, rom_fetcher);
|
||||
case Model::Plus2a: return new ConcreteMachine<Model::Plus2a>(*zx_target, rom_fetcher);
|
||||
case Model::Plus3: return new ConcreteMachine<Model::Plus3>(*zx_target, rom_fetcher);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
@@ -31,7 +31,7 @@ class Machine {
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
friend Configurable::QuickloadOption<Options>;
|
||||
public:
|
||||
bool automatic_tape_motor_control;
|
||||
bool automatic_tape_motor_control = true;
|
||||
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ? Configurable::Display::RGB : Configurable::Display::CompositeColour),
|
||||
|
||||
26
Machines/StateProducer.hpp
Normal file
26
Machines/StateProducer.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// StateProducer.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 24/04/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef State_h
|
||||
#define State_h
|
||||
|
||||
#include <memory>
|
||||
#include "../Analyser/Static/StaticAnalyser.hpp"
|
||||
|
||||
namespace MachineTypes {
|
||||
|
||||
struct StateProducer {
|
||||
// TODO.
|
||||
// virtual bool get_state(Analyser::Static::State *, [[maybe_unused]] bool advance_to_simple = false) {
|
||||
// return false;
|
||||
// }
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
#endif /* State_h */
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "../ColecoVision/ColecoVision.hpp"
|
||||
#include "../Commodore/Vic-20/Vic20.hpp"
|
||||
#include "../Electron/Electron.hpp"
|
||||
#include "../Enterprise/Enterprise.hpp"
|
||||
#include "../MasterSystem/MasterSystem.hpp"
|
||||
#include "../MSX/MSX.hpp"
|
||||
#include "../Oric/Oric.hpp"
|
||||
@@ -34,6 +35,7 @@
|
||||
#include "../../Analyser/Static/Atari2600/Target.hpp"
|
||||
#include "../../Analyser/Static/AtariST/Target.hpp"
|
||||
#include "../../Analyser/Static/Commodore/Target.hpp"
|
||||
#include "../../Analyser/Static/Enterprise/Target.hpp"
|
||||
#include "../../Analyser/Static/Macintosh/Target.hpp"
|
||||
#include "../../Analyser/Static/MSX/Target.hpp"
|
||||
#include "../../Analyser/Static/Oric/Target.hpp"
|
||||
@@ -49,7 +51,7 @@ Machine::DynamicMachine *Machine::MachineForTarget(const Analyser::Static::Targe
|
||||
|
||||
Machine::DynamicMachine *machine = nullptr;
|
||||
try {
|
||||
#define BindD(name, m) case Analyser::Machine::m: machine = new Machine::TypedDynamicMachine<name::Machine>(name::Machine::m(target, rom_fetcher)); break;
|
||||
#define BindD(name, m) case Analyser::Machine::m: machine = new Machine::TypedDynamicMachine<::name::Machine>(name::Machine::m(target, rom_fetcher)); break;
|
||||
#define Bind(m) BindD(m, m)
|
||||
switch(target->machine) {
|
||||
Bind(AmstradCPC)
|
||||
@@ -61,6 +63,7 @@ Machine::DynamicMachine *Machine::MachineForTarget(const Analyser::Static::Targe
|
||||
BindD(Coleco::Vision, ColecoVision)
|
||||
BindD(Commodore::Vic20, Vic20)
|
||||
Bind(Electron)
|
||||
Bind(Enterprise)
|
||||
Bind(MSX)
|
||||
Bind(Oric)
|
||||
BindD(Sega::MasterSystem, MasterSystem)
|
||||
@@ -127,6 +130,7 @@ std::string Machine::ShortNameForTargetMachine(const Analyser::Machine machine)
|
||||
case Analyser::Machine::AtariST: return "AtariST";
|
||||
case Analyser::Machine::ColecoVision: return "ColecoVision";
|
||||
case Analyser::Machine::Electron: return "Electron";
|
||||
case Analyser::Machine::Enterprise: return "Enterprise";
|
||||
case Analyser::Machine::Macintosh: return "Macintosh";
|
||||
case Analyser::Machine::MasterSystem: return "MasterSystem";
|
||||
case Analyser::Machine::MSX: return "MSX";
|
||||
@@ -148,6 +152,7 @@ std::string Machine::LongNameForTargetMachine(Analyser::Machine machine) {
|
||||
case Analyser::Machine::AtariST: return "Atari ST";
|
||||
case Analyser::Machine::ColecoVision: return "ColecoVision";
|
||||
case Analyser::Machine::Electron: return "Acorn Electron";
|
||||
case Analyser::Machine::Enterprise: return "Enterprise";
|
||||
case Analyser::Machine::Macintosh: return "Apple Macintosh";
|
||||
case Analyser::Machine::MasterSystem: return "Sega Master System";
|
||||
case Analyser::Machine::MSX: return "MSX";
|
||||
@@ -177,11 +182,13 @@ std::vector<std::string> Machine::AllMachines(Type type, bool long_names) {
|
||||
AddName(AppleIIgs);
|
||||
AddName(AtariST);
|
||||
AddName(Electron);
|
||||
AddName(Enterprise);
|
||||
AddName(Macintosh);
|
||||
AddName(MSX);
|
||||
AddName(Oric);
|
||||
AddName(Vic20);
|
||||
AddName(ZX8081);
|
||||
AddName(ZXSpectrum);
|
||||
}
|
||||
|
||||
#undef AddName
|
||||
@@ -200,6 +207,7 @@ std::map<std::string, std::unique_ptr<Reflection::Struct>> Machine::AllOptionsBy
|
||||
Emplace(AtariST, Atari::ST::Machine);
|
||||
Emplace(ColecoVision, Coleco::Vision::Machine);
|
||||
Emplace(Electron, Electron::Machine);
|
||||
Emplace(Enterprise, Enterprise::Machine);
|
||||
Emplace(Macintosh, Apple::Macintosh::Machine);
|
||||
Emplace(MasterSystem, Sega::MasterSystem::Machine);
|
||||
Emplace(MSX, MSX::Machine);
|
||||
@@ -225,6 +233,7 @@ std::map<std::string, std::unique_ptr<Analyser::Static::Target>> Machine::Target
|
||||
Add(AppleIIgs);
|
||||
Add(AtariST);
|
||||
AddMapped(Electron, Acorn);
|
||||
Add(Enterprise);
|
||||
Add(Macintosh);
|
||||
Add(MSX);
|
||||
Add(Oric);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "MemoryPacker.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
void Memory::PackBigEndian16(const std::vector<uint8_t> &source, uint16_t *target) {
|
||||
for(size_t c = 0; c < source.size(); c += 2) {
|
||||
target[c >> 1] = uint16_t(source[c] << 8) | uint16_t(source[c+1]);
|
||||
|
||||
545
Machines/Utility/ROMCatalogue.cpp
Normal file
545
Machines/Utility/ROMCatalogue.cpp
Normal file
@@ -0,0 +1,545 @@
|
||||
//
|
||||
// ROMCatalogue.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 01/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "ROMCatalogue.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <codecvt>
|
||||
#include <iomanip>
|
||||
#include <locale>
|
||||
#include <sstream>
|
||||
|
||||
using namespace ROM;
|
||||
|
||||
namespace {
|
||||
constexpr Name MaxName = Name::SpectrumPlus3;
|
||||
}
|
||||
|
||||
Request::Request(Name name, bool optional) {
|
||||
node.name = name;
|
||||
node.is_optional = optional;
|
||||
}
|
||||
|
||||
Request Request::append(Node::Type type, const Request &rhs) {
|
||||
// If either side is empty, act appropriately.
|
||||
if(node.empty() && !rhs.node.empty()) {
|
||||
return rhs;
|
||||
}
|
||||
if(rhs.node.empty()) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Just copy in the RHS child nodes if types match.
|
||||
if(node.type == type && rhs.node.type == type) {
|
||||
Request new_request = *this;
|
||||
new_request.node.children.insert(new_request.node.children.end(), rhs.node.children.begin(), rhs.node.children.end());
|
||||
new_request.node.sort();
|
||||
return new_request;
|
||||
}
|
||||
|
||||
// Possibly: left is appropriate request and rhs is just one more thing?
|
||||
if(node.type == type && rhs.node.type == Node::Type::One) {
|
||||
Request new_request = *this;
|
||||
new_request.node.children.push_back(rhs.node);
|
||||
new_request.node.sort();
|
||||
return new_request;
|
||||
}
|
||||
|
||||
// Or: right is appropriate request and this is just one more thing?
|
||||
if(rhs.node.type == type && node.type == Node::Type::One) {
|
||||
Request new_request = rhs;
|
||||
new_request.node.children.push_back(node);
|
||||
new_request.node.sort();
|
||||
return new_request;
|
||||
}
|
||||
|
||||
// Otherwise create a new parent node.
|
||||
Request parent;
|
||||
parent.node.type = type;
|
||||
parent.node.children.push_back(this->node);
|
||||
parent.node.children.push_back(rhs.node);
|
||||
return parent;
|
||||
}
|
||||
|
||||
Request Request::operator &&(const Request &rhs) {
|
||||
return append(Node::Type::All, rhs);
|
||||
}
|
||||
|
||||
Request Request::operator ||(const Request &rhs) {
|
||||
return append(Node::Type::Any, rhs);
|
||||
}
|
||||
|
||||
bool Request::validate(Map &map) const {
|
||||
return node.validate(map);
|
||||
}
|
||||
|
||||
std::vector<ROM::Description> Request::all_descriptions() const {
|
||||
std::vector<Description> result;
|
||||
node.add_descriptions(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void Request::Node::add_descriptions(std::vector<Description> &result) const {
|
||||
if(type == Type::One) {
|
||||
result.push_back(name);
|
||||
return;
|
||||
}
|
||||
|
||||
for(const auto &child: children) {
|
||||
child.add_descriptions(result);
|
||||
}
|
||||
}
|
||||
|
||||
Request Request::subtract(const ROM::Map &map) const {
|
||||
Request copy(*this);
|
||||
if(copy.node.subtract(map)) {
|
||||
copy.node.name = Name::None;
|
||||
copy.node.type = Node::Type::One;
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
bool Request::Node::subtract(const ROM::Map &map) {
|
||||
switch(type) {
|
||||
case Type::One:
|
||||
return map.find(name) != map.end();
|
||||
|
||||
default: {
|
||||
bool has_all = true;
|
||||
bool has_any = false;
|
||||
|
||||
auto iterator = children.begin();
|
||||
while(iterator != children.end()) {
|
||||
const bool did_subtract = iterator->subtract(map);
|
||||
has_all &= did_subtract;
|
||||
has_any |= did_subtract;
|
||||
if(did_subtract) {
|
||||
iterator = children.erase(iterator);
|
||||
} else {
|
||||
++iterator;
|
||||
}
|
||||
}
|
||||
|
||||
return (type == Type::All && has_all) || (type == Type::Any && has_any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Request::empty() {
|
||||
return node.type == Node::Type::One && node.name == Name::None;
|
||||
}
|
||||
|
||||
bool Request::Node::validate(Map &map) const {
|
||||
// Leaf nodes are easy: check that the named ROM is present,
|
||||
// unless it's optional, in which case it is always valid.
|
||||
//
|
||||
// If it is present, make sure it's the proper size.
|
||||
if(type == Type::One) {
|
||||
auto rom = map.find(name);
|
||||
if(rom == map.end()) {
|
||||
return is_optional;
|
||||
}
|
||||
|
||||
const Description description(name);
|
||||
rom->second.resize(description.size);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// This is a collection node then. Check for both any or all
|
||||
// simultaneously, since all nodes will need to be visited
|
||||
// regardless of any/all in order to ensure proper sizing.
|
||||
bool has_all = true;
|
||||
bool has_any = false;
|
||||
|
||||
for(const auto &child: children) {
|
||||
const bool is_valid = child.validate(map);
|
||||
has_all &= is_valid;
|
||||
has_any |= is_valid;
|
||||
}
|
||||
|
||||
return (type == Type::Any && has_any) || (type == Type::All && has_all);
|
||||
}
|
||||
|
||||
void Request::visit(
|
||||
const std::function<void(ListType, size_t)> &enter_list,
|
||||
const std::function<void(void)> &exit_list,
|
||||
const std::function<void(ROM::Request::ListType, const ROM::Description &, bool, size_t)> &add_item
|
||||
) const {
|
||||
node.visit(enter_list, exit_list, add_item);
|
||||
}
|
||||
|
||||
void Request::visit(
|
||||
const std::function<void(LineItem, ListType, int, const ROM::Description *, bool, size_t)> &add_item
|
||||
) const {
|
||||
int indentation_level = 0;
|
||||
node.visit(
|
||||
[&indentation_level, &add_item] (ROM::Request::ListType type, size_t size) {
|
||||
add_item(LineItem::NewList, type, indentation_level, nullptr, false, size);
|
||||
++indentation_level;
|
||||
},
|
||||
[&indentation_level] {
|
||||
--indentation_level;
|
||||
},
|
||||
[&indentation_level, &add_item] (ROM::Request::ListType type, const ROM::Description &rom, bool is_optional, size_t remaining) {
|
||||
add_item(LineItem::Description, type, indentation_level, &rom, is_optional, remaining);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void Request::Node::visit(
|
||||
const std::function<void(ListType, size_t)> &enter_list,
|
||||
const std::function<void(void)> &exit_list,
|
||||
const std::function<void(ROM::Request::ListType type, const ROM::Description &, bool is_optional, size_t remaining)> &add_item
|
||||
) const {
|
||||
switch(type) {
|
||||
case Type::One:
|
||||
enter_list(ListType::Single, 1);
|
||||
add_item(ROM::Request::ListType::Any, Description(name), is_optional, 0);
|
||||
exit_list();
|
||||
break;
|
||||
|
||||
case Type::Any:
|
||||
case Type::All: {
|
||||
const ListType list_type = type == Type::Any ? ListType::Any : ListType::All;
|
||||
enter_list(list_type, children.size());
|
||||
for(size_t index = 0; index < children.size(); index++) {
|
||||
auto &child = children[index];
|
||||
|
||||
if(child.type == Type::One) {
|
||||
add_item(list_type, Description(child.name), child.is_optional, children.size() - 1 - index);
|
||||
} else {
|
||||
child.visit(enter_list, exit_list, add_item);
|
||||
}
|
||||
}
|
||||
exit_list();
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Description> ROM::all_descriptions() {
|
||||
std::vector<Description> result;
|
||||
for(int name = 1; name <= MaxName; name++) {
|
||||
result.push_back(Description(ROM::Name(name)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<Description> Description::from_crc(uint32_t crc32) {
|
||||
for(int name = 1; name <= MaxName; name++) {
|
||||
const Description candidate = Description(ROM::Name(name));
|
||||
|
||||
const auto found_crc = std::find(candidate.crc32s.begin(), candidate.crc32s.end(), crc32);
|
||||
if(found_crc != candidate.crc32s.end()) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string Description::description(int flags) const {
|
||||
std::stringstream output;
|
||||
|
||||
// If there are no CRCs, don't output them.
|
||||
if(crc32s.empty()) flags &= ~ DescriptionFlag::CRC;
|
||||
|
||||
// Print the file name(s) and the descriptive name.
|
||||
if(flags & DescriptionFlag::Filename) {
|
||||
flags &= ~DescriptionFlag::Filename;
|
||||
|
||||
output << machine_name << '/';
|
||||
if(file_names.size() == 1) {
|
||||
output << file_names[0];
|
||||
} else {
|
||||
output << "{";
|
||||
bool is_first = true;
|
||||
for(const auto &file_name: file_names) {
|
||||
if(!is_first) output << " or ";
|
||||
output << file_name;
|
||||
is_first = false;
|
||||
}
|
||||
output << "}";
|
||||
}
|
||||
output << " (" << descriptive_name;
|
||||
if(!flags) {
|
||||
output << ")";
|
||||
return output.str();
|
||||
}
|
||||
output << "; ";
|
||||
} else {
|
||||
output << descriptive_name;
|
||||
if(!flags) {
|
||||
return output.str();
|
||||
}
|
||||
output << " (";
|
||||
}
|
||||
|
||||
// Print the size.
|
||||
if(flags & DescriptionFlag::Size) {
|
||||
flags &= ~DescriptionFlag::Size;
|
||||
output << size << " bytes";
|
||||
|
||||
if(!flags) {
|
||||
output << ")";
|
||||
return output.str();
|
||||
}
|
||||
output << "; ";
|
||||
}
|
||||
|
||||
// Print the CRC(s).
|
||||
if(flags & DescriptionFlag::CRC) {
|
||||
flags &= ~DescriptionFlag::CRC;
|
||||
|
||||
output << ((crc32s.size() > 1) ? "usual crc32s: " : "usual crc32: ");
|
||||
bool is_first = true;
|
||||
for(const auto crc32: crc32s) {
|
||||
if(!is_first) output << ", ";
|
||||
is_first = false;
|
||||
output << std::hex << std::setfill('0') << std::setw(8) << crc32;
|
||||
}
|
||||
|
||||
if(!flags) {
|
||||
output << ")";
|
||||
return output.str();
|
||||
}
|
||||
}
|
||||
|
||||
return output.str();
|
||||
}
|
||||
|
||||
std::wstring Request::description(int description_flags, wchar_t bullet_point) {
|
||||
std::wstringstream output;
|
||||
std::wstring_convert<std::codecvt_utf8<wchar_t>> wstring_converter;
|
||||
|
||||
visit(
|
||||
[&output, description_flags, bullet_point, &wstring_converter] (ROM::Request::LineItem item, ROM::Request::ListType type, int indentation_level, const ROM::Description *description, bool is_optional, size_t remaining) {
|
||||
if(indentation_level) {
|
||||
output << std::endl;
|
||||
for(int c = 0; c < indentation_level; c++) output << '\t';
|
||||
output << bullet_point << ' ';
|
||||
}
|
||||
|
||||
switch(item) {
|
||||
case ROM::Request::LineItem::NewList:
|
||||
if(remaining > 1) {
|
||||
if(!indentation_level) output << " ";
|
||||
switch(type) {
|
||||
default:
|
||||
case ROM::Request::ListType::All: output << "all of:"; break;
|
||||
case ROM::Request::ListType::Any:
|
||||
if(remaining == 2) {
|
||||
output << "either of:";
|
||||
} else {
|
||||
output << "any of:";
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
output << ":";
|
||||
}
|
||||
break;
|
||||
|
||||
case ROM::Request::LineItem::Description:
|
||||
if(is_optional) output << "optionally, ";
|
||||
|
||||
output << wstring_converter.from_bytes(description->description(description_flags));
|
||||
|
||||
if(remaining) {
|
||||
output << ";";
|
||||
if(remaining == 1) {
|
||||
output << ((type == ROM::Request::ListType::All) ? " and" : " or");
|
||||
}
|
||||
} else {
|
||||
output << ".";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return output.str();
|
||||
}
|
||||
|
||||
|
||||
Description::Description(Name name) {
|
||||
switch(name) {
|
||||
default: assert(false); break;
|
||||
|
||||
case Name::AMSDOS: *this = Description(name, "AmstradCPC", "the Amstrad Disk Operating System", "amsdos.rom", 16*1024, 0x1fe22ecdu); break;
|
||||
case Name::CPC464Firmware: *this = Description(name, "AmstradCPC", "the CPC 464 firmware", "os464.rom", 16*1024, 0x815752dfu); break;
|
||||
case Name::CPC464BASIC: *this = Description(name, "AmstradCPC", "the CPC 464 BASIC ROM", "basic464.rom", 16*1024, 0x7d9a3bacu); break;
|
||||
case Name::CPC664Firmware: *this = Description(name, "AmstradCPC", "the CPC 664 firmware", "os664.rom", 16*1024, 0x3f5a6dc4u); break;
|
||||
case Name::CPC664BASIC: *this = Description(name, "AmstradCPC", "the CPC 664 BASIC ROM", "basic664.rom", 16*1024, 0x32fee492u); break;
|
||||
case Name::CPC6128Firmware: *this = Description(name, "AmstradCPC", "the CPC 6128 firmware", "os6128.rom", 16*1024, 0x0219bb74u); break;
|
||||
case Name::CPC6128BASIC: *this = Description(name, "AmstradCPC", "the CPC 6128 BASIC ROM", "basic6128.rom", 16*1024, 0xca6af63du); break;
|
||||
|
||||
case Name::AppleIIEnhancedE: *this = Description(name, "AppleII", "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942u); break;
|
||||
case Name::AppleIIe: *this = Description(name, "AppleII", "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18du); break;
|
||||
case Name::AppleIIPlus: *this = Description(name, "AppleII", "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26u); break;
|
||||
case Name::AppleIIOriginal: *this = Description(name, "AppleII", "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588u); break;
|
||||
case Name::AppleIICharacter: *this = Description(name, "AppleII", "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6u); break;
|
||||
case Name::AppleIIeCharacter: *this = Description(name, "AppleII", "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1u); break;
|
||||
case Name::AppleIIEnhancedECharacter:
|
||||
*this = Description(name, "AppleII", "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014du);
|
||||
break;
|
||||
|
||||
case Name::AppleIIgsROM00: /* TODO */
|
||||
case Name::AppleIIgsROM01: *this = Description(name, "AppleIIgs", "the Apple IIgs ROM01", "apple2gs.rom", 128*1024, 0x42f124b0u); break;
|
||||
case Name::AppleIIgsROM03: *this = Description(name, "AppleIIgs", "the Apple IIgs ROM03", "apple2gs.rom2", 256*1024, 0xde7ddf29u); break;
|
||||
case Name::AppleIIgsCharacter: *this = Description(name, "AppleIIgs", "the Apple IIgs character ROM", "apple2gs.chr", 4*1024, 0x91e53cd8u); break;
|
||||
case AppleIIgsMicrocontrollerROM03:
|
||||
*this = Description(name, "AppleIIgs", "the Apple IIgs ROM03 ADB microcontroller ROM", "341s0632-2", 4*1024, 0xe1c11fb0u);
|
||||
break;
|
||||
|
||||
case Name::DiskIIBoot16Sector:
|
||||
*this = Description(name, "DiskII", "the Disk II 16-sector boot ROM", "boot-16.rom", 256, 0xce7144f6u);
|
||||
break;
|
||||
case Name::DiskIIStateMachine16Sector:
|
||||
*this = Description(name, "DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, std::initializer_list<uint32_t>{ 0x9796a238, 0xb72a2c70 } );
|
||||
break;
|
||||
case Name::DiskIIBoot13Sector:
|
||||
*this = Description(name, "DiskII", "the Disk II 13-sector boot ROM", "boot-13.rom", 256, 0xd34eb2ffu);
|
||||
break;
|
||||
case Name::DiskIIStateMachine13Sector:
|
||||
*this = Description(name, "DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620u);
|
||||
break;
|
||||
|
||||
case Name::EnterpriseEXOS10: {
|
||||
const std::initializer_list<std::string> filenames = {"exos10.bin", "Exos (198x)(Enterprise).bin"};
|
||||
*this = Description(name, "Enterprise", "the Enterprise EXOS ROM v1.0", filenames, 32 * 1024, 0x30b26387u);
|
||||
} break;
|
||||
case Name::EnterpriseEXOS20: {
|
||||
const std::initializer_list<std::string> filenames = {"exos20.bin", "Expandible OS v2.0 (1984)(Intelligent Software).bin"};
|
||||
*this = Description(name, "Enterprise", "the Enterprise EXOS ROM v2.0", filenames, 32 * 1024, 0xd421795fu);
|
||||
} break;
|
||||
case Name::EnterpriseEXOS21: {
|
||||
const std::initializer_list<std::string> filenames = {"exos21.bin", "Expandible OS v2.1 (1985)(Intelligent Software).bin"};
|
||||
*this = Description(name, "Enterprise", "the Enterprise EXOS ROM v2.1", filenames, 32 * 1024, 0x982a3b44u);
|
||||
} break;
|
||||
case Name::EnterpriseEXOS23: {
|
||||
const std::initializer_list<std::string> filenames = {"exos23.bin", "Expandible OS v2.3 (1987)(Intelligent Software).bin"};
|
||||
*this = Description(name, "Enterprise", "the Enterprise EXOS ROM v2.1", filenames, 64 * 1024, 0x24838410u);
|
||||
} break;
|
||||
|
||||
case Name::EnterpriseBASIC10: {
|
||||
const std::initializer_list<std::string> filenames = {"basic10.bin"};
|
||||
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v1.0", filenames, 16 * 1024, 0xd62e4fb7u);
|
||||
} break;
|
||||
case Name::EnterpriseBASIC10Part1: {
|
||||
const std::initializer_list<std::string> filenames = {"BASIC 1.0 - EPROM 1-2 (198x)(Enterprise).bin"};
|
||||
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v1.0, Part 1", filenames, 8193, 0x37bf48e1u);
|
||||
} break;
|
||||
case Name::EnterpriseBASIC10Part2: {
|
||||
const std::initializer_list<std::string> filenames = {"BASIC 1.0 - EPROM 2-2 (198x)(Enterprise).bin"};
|
||||
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v1.0, Part 2", filenames, 8193, 0xc5298c79u);
|
||||
} break;
|
||||
case Name::EnterpriseBASIC11: {
|
||||
const std::initializer_list<std::string> filenames = {"basic11.bin"};
|
||||
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v1.1", filenames, 16 * 1024, 0x683cf455u);
|
||||
} break;
|
||||
case Name::EnterpriseBASIC11Suffixed: {
|
||||
const std::initializer_list<std::string> filenames = {"BASIC 1.1 - EPROM 1.1 (198x)(Enterprise).bin"};
|
||||
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v1.1, with trailing byte", filenames, 16385, 0xc96b7602u);
|
||||
} break;
|
||||
case Name::EnterpriseBASIC21: {
|
||||
const std::initializer_list<std::string> filenames = {
|
||||
"basic21.bin",
|
||||
"BASIC Interpreter v2.1 (1985)(Intelligent Software).bin",
|
||||
"BASIC Interpreter v2.1 (1985)(Intelligent Software)[a].bin"
|
||||
};
|
||||
const std::initializer_list<uint32_t> crcs = { 0x55f96251, 0x683cf455 };
|
||||
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v2.1", filenames, 16 * 1024, crcs);
|
||||
} break;
|
||||
|
||||
case Name::EnterpriseEPDOS: {
|
||||
const std::initializer_list<std::string> filenames = {"epdos.bin", "EPDOS v1.7 (19xx)(Haluska, Laszlo).bin"};
|
||||
*this = Description(name, "Enterprise", "the Enterprise EPDOS ROM", filenames, 32 * 1024, 0x201319ebu);
|
||||
} break;
|
||||
case Name::EnterpriseEXDOS: {
|
||||
const std::initializer_list<std::string> filenames = {"exdos.bin", "EX-DOS EPROM (198x)(Enterprise).bin"};
|
||||
*this = Description(name, "Enterprise", "the Enterprise EXDOS ROM", filenames, 16 * 1024, 0xe6daa0e9u);
|
||||
} break;
|
||||
|
||||
case Name::Macintosh128k: *this = Description(name, "Macintosh", "the Macintosh 128k ROM", "mac128k.rom", 64*1024, 0x6d0c8a28u); break;
|
||||
case Name::Macintosh512k: *this = Description(name, "Macintosh", "the Macintosh 512k ROM", "mac512k.rom", 64*1024, 0xcf759e0d); break;
|
||||
case Name::MacintoshPlus: {
|
||||
const std::initializer_list<uint32_t> crcs = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e };
|
||||
*this = Description(name, "Macintosh", "the Macintosh Plus ROM", "macplus.rom", 128*1024, crcs);
|
||||
} break;
|
||||
|
||||
case Name::AtariSTTOS100: *this = Description(name, "AtariST", "the UK TOS 1.00 ROM", "tos100.img", 192*1024, 0x1a586c64u); break;
|
||||
case Name::AtariSTTOS104: *this = Description(name, "AtariST", "the UK TOS 1.04 ROM", "tos104.img", 192*1024, 0xa50d1d43u); break;
|
||||
case Name::AtariSTEmuTOS192: *this = Description(name, "AtariST", "the UK EmuTOS 1.92 ROM", "etos192uk.img", 192*1024, 0xfc3b9e61u); break;
|
||||
|
||||
case Name::ColecoVisionBIOS:
|
||||
*this = Description(name, "ColecoVision", "the ColecoVision BIOS", "coleco.rom", 8*1024, 0x3aa93ef3u);
|
||||
break;
|
||||
|
||||
case Name::ZX80: *this = Description(name, "ZX8081", "the ZX80 BASIC ROM", "zx80.rom", 4 * 1024, 0x4c7fc597u); break;
|
||||
case Name::ZX81: *this = Description(name, "ZX8081", "the ZX81 BASIC ROM", "zx81.rom", 8 * 1024, 0x4b1dd6ebu); break;
|
||||
|
||||
case Name::Spectrum48k: *this = Description(name, "ZXSpectrum", "the 48kb ROM", "48.rom", 16 * 1024, 0xddee531fu); break;
|
||||
case Name::Spectrum128k: *this = Description(name, "ZXSpectrum", "the 128kb ROM", "128.rom", 32 * 1024, 0x2cbe8995u); break;
|
||||
case Name::SpecrumPlus2: *this = Description(name, "ZXSpectrum", "the +2 ROM", "plus2.rom", 32 * 1024, 0xe7a517dcu); break;
|
||||
case Name::SpectrumPlus3: {
|
||||
const std::initializer_list<uint32_t> crcs = { 0x96e3c17a, 0xbe0d9ec4 };
|
||||
*this = Description(name, "ZXSpectrum", "the +2a/+3 ROM", "plus3.rom", 64 * 1024, crcs);
|
||||
} break;
|
||||
|
||||
case Name::AcornBASICII: *this = Description(name, "Electron", "the Acorn BASIC II ROM", "basic.rom", 16*1024, 0x79434781u); break;
|
||||
case Name::PRESADFSSlot1: *this = Description(name, "Electron", "the E00 ADFS ROM, first slot", "ADFS-E00_1.rom", 16*1024, 0x51523993u); break;
|
||||
case Name::PRESADFSSlot2: *this = Description(name, "Electron", "the E00 ADFS ROM, second slot", "ADFS-E00_2.rom", 16*1024, 0x8d17de0eu); break;
|
||||
case Name::AcornADFS: *this = Description(name, "Electron", "the Acorn ADFS ROM", "adfs.rom", 16*1024, 0x3289bdc6u); break;
|
||||
case Name::Acorn1770DFS: *this = Description(name, "Electron", "the 1770 DFS ROM", "DFS-1770-2.20.rom", 16*1024, 0xf3dc9bc5u); break;
|
||||
case Name::PRESAdvancedPlus6:
|
||||
*this = Description(name, "Electron", "the 8kb Advanced Plus 6 ROM", "AP6v133.rom", 8*1024, 0xe0013cfcu);
|
||||
break;
|
||||
case Name::AcornElectronMOS100:
|
||||
*this = Description(name, "Electron", "the Electron MOS ROM v1.00", "os.rom", 16*1024, 0xbf63fb1fu);
|
||||
break;
|
||||
|
||||
case Name::MasterSystemJapaneseBIOS: *this = Description(name, "MasterSystem", "the Japanese Master System BIOS", "japanese-bios.sms", 8*1024, 0x48d44a13u); break;
|
||||
case Name::MasterSystemWesternBIOS: *this = Description(name, "MasterSystem", "the European/US Master System BIOS", "bios.sms", 8*1024, 0x0072ed54u); break;
|
||||
|
||||
case Name::Commodore1540: *this = Description(name, "Commodore1540", "the 1540 ROM", "1540.bin", 16*1024, 0x718d42b1u); break;
|
||||
case Name::Commodore1541: *this = Description(name, "Commodore1540", "the 1541 ROM", "1541.bin", 16*1024, 0xfb760019); break;
|
||||
|
||||
case Name::Vic20BASIC: *this = Description(name, "Vic20", "the VIC-20 BASIC ROM", "basic.bin", 8*1024, 0xdb4c43c1u); break;
|
||||
case Name::Vic20EnglishCharacters: *this = Description(name, "Vic20", "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6u); break;
|
||||
case Name::Vic20EnglishPALKernel: *this = Description(name, "Vic20", "the English-language PAL VIC-20 kernel ROM", "kernel-pal.bin", 8*1024, 0x4be07cb4u); break;
|
||||
case Name::Vic20EnglishNTSCKernel: *this = Description(name, "Vic20", "the English-language NTSC VIC-20 kernel ROM", "kernel-ntsc.bin", 8*1024, 0xe5e7c174u); break;
|
||||
case Name::Vic20DanishCharacters: *this = Description(name, "Vic20", "the Danish VIC-20 character ROM", "characters-danish.bin", 4*1024, 0x7fc11454u); break;
|
||||
case Name::Vic20DanishKernel: *this = Description(name, "Vic20", "the Danish VIC-20 kernel ROM", "kernel-danish.bin", 8*1024, 0x02adaf16u); break;
|
||||
case Name::Vic20JapaneseCharacters: *this = Description(name, "Vic20", "the Japanese VIC-20 character ROM", "characters-japanese.bin", 4*1024, 0xfcfd8a4bu); break;
|
||||
case Name::Vic20JapaneseKernel: *this = Description(name, "Vic20", "the Japanese VIC-20 kernel ROM", "kernel-japanese.bin", 8*1024, 0x336900d7u); break;
|
||||
case Name::Vic20SwedishCharacters: *this = Description(name, "Vic20", "the Swedish VIC-20 character ROM", "characters-swedish.bin", 4*1024, 0xd808551du); break;
|
||||
case Name::Vic20SwedishKernel: *this = Description(name, "Vic20", "the Swedish VIC-20 kernel ROM", "kernel-swedish.bin", 8*1024, 0xb2a60662u); break;
|
||||
|
||||
case Name::OricColourROM: *this = Description(name, "Oric", "the Oric colour ROM", "colour.rom", 128, 0xd50fca65u); break;
|
||||
case Name::OricBASIC10: *this = Description(name, "Oric", "Oric BASIC 1.0", "basic10.rom", 16*1024, 0xf18710b4u); break;
|
||||
case Name::OricBASIC11: *this = Description(name, "Oric", "Oric BASIC 1.1", "basic11.rom", 16*1024, 0xc3a92befu); break;
|
||||
case Name::OricPravetzBASIC: *this = Description(name, "Oric", "Pravetz BASIC", "pravetz.rom", 16*1024, 0x58079502u); break;
|
||||
case Name::OricByteDrive500: *this = Description(name, "Oric", "the Oric Byte Drive 500 ROM", "bd500.rom", 8*1024, 0x61952e34u); break;
|
||||
case Name::OricJasmin: *this = Description(name, "Oric", "the Oric Jasmin ROM", "jasmin.rom", 2*1024, 0x37220e89u); break;
|
||||
case Name::OricMicrodisc: *this = Description(name, "Oric", "the Oric Microdisc ROM", "microdisc.rom", 8*1024, 0xa9664a9cu); break;
|
||||
case Name::Oric8DOSBoot: *this = Description(name, "Oric", "the 8DOS boot ROM", "8dos.rom", 512, 0x49a74c06u); break;
|
||||
|
||||
case Name::MSXGenericBIOS: *this = Description(name, "MSX", "any MSX BIOS", "msx.rom", 32*1024, 0x94ee12f3u); break;
|
||||
case Name::MSXJapaneseBIOS: *this = Description(name, "MSX", "a Japanese MSX BIOS", "msx-japanese.rom", 32*1024, 0xee229390u); break;
|
||||
case Name::MSXAmericanBIOS: *this = Description(name, "MSX", "an American MSX BIOS", "msx-american.rom", 32*1024, 0u); break;
|
||||
case Name::MSXEuropeanBIOS: *this = Description(name, "MSX", "a European MSX BIOS", "msx-european.rom", 32*1024, 0u); break;
|
||||
case Name::MSXDOS: *this = Description(name, "MSX", "the MSX-DOS ROM", "disk.rom", 16*1024, 0x721f61dfu); break;
|
||||
|
||||
case Name::SinclairQLJS:
|
||||
*this = Description(name, "SinclairQL", "the Sinclair QL 'JS' ROM", "js.rom", 48*1024, 0x0f95aab5u);
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
289
Machines/Utility/ROMCatalogue.hpp
Normal file
289
Machines/Utility/ROMCatalogue.hpp
Normal file
@@ -0,0 +1,289 @@
|
||||
//
|
||||
// ROMCatalogue.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 01/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef ROMCatalogue_hpp
|
||||
#define ROMCatalogue_hpp
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ROM {
|
||||
|
||||
enum Name {
|
||||
None,
|
||||
|
||||
// Acorn.
|
||||
AcornBASICII,
|
||||
AcornElectronMOS100,
|
||||
PRESADFSSlot1,
|
||||
PRESADFSSlot2,
|
||||
AcornADFS,
|
||||
PRESAdvancedPlus6,
|
||||
Acorn1770DFS,
|
||||
|
||||
// Amstrad CPC.
|
||||
AMSDOS,
|
||||
CPC464Firmware, CPC464BASIC,
|
||||
CPC664Firmware, CPC664BASIC,
|
||||
CPC6128Firmware, CPC6128BASIC,
|
||||
|
||||
// Apple II.
|
||||
AppleIIOriginal,
|
||||
AppleIIPlus,
|
||||
AppleIICharacter,
|
||||
AppleIIe,
|
||||
AppleIIeCharacter,
|
||||
AppleIIEnhancedE,
|
||||
AppleIIEnhancedECharacter,
|
||||
|
||||
// Apple IIgs.
|
||||
AppleIIgsROM00,
|
||||
AppleIIgsROM01,
|
||||
AppleIIgsROM03,
|
||||
AppleIIgsMicrocontrollerROM03,
|
||||
AppleIIgsCharacter,
|
||||
|
||||
// Atari ST.
|
||||
AtariSTTOS100,
|
||||
AtariSTTOS104,
|
||||
AtariSTEmuTOS192,
|
||||
|
||||
// ColecoVision.
|
||||
ColecoVisionBIOS,
|
||||
|
||||
// Commodore 1540/1541.
|
||||
Commodore1540,
|
||||
Commodore1541,
|
||||
|
||||
// Disk II.
|
||||
DiskIIStateMachine16Sector,
|
||||
DiskIIBoot16Sector,
|
||||
DiskIIStateMachine13Sector,
|
||||
DiskIIBoot13Sector,
|
||||
|
||||
// Enterprise.
|
||||
EnterpriseEXOS10,
|
||||
EnterpriseEXOS20,
|
||||
EnterpriseEXOS21,
|
||||
EnterpriseEXOS23,
|
||||
|
||||
EnterpriseBASIC10,
|
||||
EnterpriseBASIC10Part1,
|
||||
EnterpriseBASIC10Part2,
|
||||
EnterpriseBASIC11,
|
||||
EnterpriseBASIC11Suffixed,
|
||||
EnterpriseBASIC21,
|
||||
|
||||
EnterpriseEPDOS,
|
||||
EnterpriseEXDOS,
|
||||
|
||||
// Macintosh.
|
||||
Macintosh128k,
|
||||
Macintosh512k,
|
||||
MacintoshPlus,
|
||||
|
||||
// Master System.
|
||||
MasterSystemJapaneseBIOS,
|
||||
MasterSystemWesternBIOS,
|
||||
|
||||
// MSX.
|
||||
MSXGenericBIOS,
|
||||
MSXJapaneseBIOS,
|
||||
MSXAmericanBIOS,
|
||||
MSXEuropeanBIOS,
|
||||
MSXDOS,
|
||||
|
||||
// Oric.
|
||||
OricColourROM,
|
||||
OricBASIC10,
|
||||
OricBASIC11,
|
||||
OricPravetzBASIC,
|
||||
OricByteDrive500,
|
||||
OricJasmin,
|
||||
OricMicrodisc,
|
||||
Oric8DOSBoot,
|
||||
|
||||
// Sinclair QL.
|
||||
SinclairQLJS,
|
||||
|
||||
// Vic-20.
|
||||
Vic20BASIC,
|
||||
Vic20EnglishCharacters,
|
||||
Vic20EnglishPALKernel,
|
||||
Vic20EnglishNTSCKernel,
|
||||
Vic20DanishCharacters,
|
||||
Vic20DanishKernel,
|
||||
Vic20JapaneseCharacters,
|
||||
Vic20JapaneseKernel,
|
||||
Vic20SwedishCharacters,
|
||||
Vic20SwedishKernel,
|
||||
|
||||
// ZX80/81.
|
||||
ZX80,
|
||||
ZX81,
|
||||
|
||||
// ZX Spectrum.
|
||||
Spectrum48k,
|
||||
Spectrum128k,
|
||||
SpecrumPlus2,
|
||||
SpectrumPlus3,
|
||||
|
||||
};
|
||||
|
||||
using Map = std::map<ROM::Name, std::vector<uint8_t>>;
|
||||
|
||||
struct Description {
|
||||
/// The ROM's enum name.
|
||||
Name name = Name::None;
|
||||
/// The machine with which this ROM is associated, in a form that is safe for using as
|
||||
/// part of a file name.
|
||||
std::string machine_name;
|
||||
/// A descriptive name for this ROM, suitable for use in a bullet-point list, a bracket
|
||||
/// clause, etc, e.g. "the Electron MOS 1.0".
|
||||
std::string descriptive_name;
|
||||
/// All idiomatic file name for this ROM, e.g. "os10.rom".
|
||||
std::vector<std::string> file_names;
|
||||
/// The expected size of this ROM in bytes, e.g. 32768.
|
||||
size_t size = 0;
|
||||
/// CRC32s for all known acceptable copies of this ROM; intended to allow a host platform
|
||||
/// to test user-provided ROMs of unknown provenance. **Not** intended to be used
|
||||
/// to exclude ROMs where the user's intent is otherwise clear.
|
||||
std::set<uint32_t> crc32s;
|
||||
|
||||
/// Constructs the @c Description that correlates to @c name.
|
||||
Description(Name name);
|
||||
|
||||
/// Constructs the @c Description that correlates to @c crc32, if any.
|
||||
static std::optional<Description> from_crc(uint32_t crc32);
|
||||
|
||||
enum DescriptionFlag {
|
||||
Size = 1 << 0,
|
||||
CRC = 1 << 1,
|
||||
Filename = 1 << 2,
|
||||
};
|
||||
|
||||
/// Provides a single-line of text describing this ROM, including the usual base text
|
||||
/// plus all the fields provided as @c flags .
|
||||
std::string description(int flags) const;
|
||||
|
||||
private:
|
||||
template <typename FileNameT, typename CRC32T> Description(
|
||||
Name name, std::string machine_name, std::string descriptive_name, FileNameT file_names, size_t size, CRC32T crc32s = uint32_t(0)
|
||||
) : name{name}, machine_name{machine_name}, descriptive_name{descriptive_name}, file_names{file_names}, size{size}, crc32s{crc32s} {
|
||||
// Slightly lazy: deal with the case where the constructor wasn't provided with any
|
||||
// CRCs by spotting that the set has exactly one member, which has value 0. The alternative
|
||||
// would be to provide a partial specialisation that never put anything into the set.
|
||||
if(this->crc32s.size() == 1 && !*this->crc32s.begin()) {
|
||||
this->crc32s.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// @returns a vector of all possible instances of ROM::Description — i.e. descriptions of every ROM
|
||||
/// currently known to the ROM catalogue.
|
||||
std::vector<Description> all_descriptions();
|
||||
|
||||
struct Request {
|
||||
Request(Name name, bool optional = false);
|
||||
Request() {}
|
||||
|
||||
/// Forms the request that would be satisfied by @c this plus the right-hand side.
|
||||
Request operator &&(const Request &);
|
||||
|
||||
/// Forms the request that would be satisfied by either @c this or the right-hand side.
|
||||
Request operator ||(const Request &);
|
||||
|
||||
/// Inspects the ROMMap to ensure that it satisfies this @c Request.
|
||||
/// @c returns @c true if the request is satisfied; @c false otherwise.
|
||||
///
|
||||
/// All ROMs in the map will be resized to their idiomatic sizes.
|
||||
bool validate(Map &) const;
|
||||
|
||||
/// Returns a flattened array of all @c ROM::Descriptions that relate to anything
|
||||
/// anywhere in this ROM request.
|
||||
std::vector<Description> all_descriptions() const;
|
||||
|
||||
/// @returns @c true if this request is empty, i.e. would be satisfied with no ROMs; @c false otherwise.
|
||||
bool empty();
|
||||
|
||||
/// @returns what remains of this ROM request given that everything in @c map has been found.
|
||||
Request subtract(const ROM::Map &map) const;
|
||||
|
||||
enum class ListType {
|
||||
Any, All, Single
|
||||
};
|
||||
void visit(
|
||||
const std::function<void(ListType, size_t size)> &enter_list,
|
||||
const std::function<void(void)> &exit_list,
|
||||
const std::function<void(ROM::Request::ListType type, const ROM::Description &, bool is_optional, size_t remaining)> &add_item
|
||||
) const;
|
||||
|
||||
enum class LineItem {
|
||||
NewList, Description
|
||||
};
|
||||
void visit(
|
||||
const std::function<void(LineItem, ListType, int level, const ROM::Description *, bool is_optional, size_t remaining)> &add_item
|
||||
) const;
|
||||
|
||||
/// @returns a full bullet-pointed list of the requirements of this request, including
|
||||
/// appropriate conjuntives. This text is intended to be glued to the end of an opening
|
||||
/// portion of a sentence, e.g. "Please supply" + request.description(0, L'*').
|
||||
std::wstring description(int description_flags, wchar_t bullet_point);
|
||||
|
||||
private:
|
||||
struct Node {
|
||||
enum class Type {
|
||||
Any, All, One
|
||||
};
|
||||
Type type = Type::One;
|
||||
Name name = Name::None;
|
||||
/// @c true if this ROM is optional for machine startup. Generally indicates something
|
||||
/// that would make emulation more accurate, but not sufficiently so to make it
|
||||
/// a necessity.
|
||||
bool is_optional = false;
|
||||
std::vector<Node> children;
|
||||
|
||||
bool empty() const {
|
||||
return type == Type::One && name == Name::None;
|
||||
}
|
||||
|
||||
void add_descriptions(std::vector<Description> &) const;
|
||||
bool validate(Map &) const;
|
||||
void visit(
|
||||
const std::function<void(ListType, size_t)> &enter_list,
|
||||
const std::function<void(void)> &exit_list,
|
||||
const std::function<void(ROM::Request::ListType type, const ROM::Description &, bool is_optional, size_t remaining)> &add_item
|
||||
) const;
|
||||
bool subtract(const ROM::Map &map);
|
||||
void sort() {
|
||||
// Don't do a full sort, but move anything optional to the back.
|
||||
// This makes them print more nicely; it's a human-facing tweak only.
|
||||
ssize_t index = ssize_t(children.size() - 1);
|
||||
bool has_seen_non_optional = false;
|
||||
while(index >= 0) {
|
||||
has_seen_non_optional |= !children[size_t(index)].is_optional;
|
||||
if(children[size_t(index)].is_optional && has_seen_non_optional) {
|
||||
std::rotate(children.begin() + index, children.begin() + index + 1, children.end());
|
||||
}
|
||||
--index;
|
||||
}
|
||||
}
|
||||
};
|
||||
Node node;
|
||||
|
||||
Request append(Node::Type type, const Request &rhs);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* ROMCatalogue_hpp */
|
||||
@@ -131,13 +131,3 @@ bool Typer::type_next_character() {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: - Character mapper
|
||||
|
||||
const uint16_t *CharacterMapper::table_lookup_sequence_for_character(const KeySequence *sequences, std::size_t length, char character) const {
|
||||
std::size_t ucharacter = size_t((unsigned char)character);
|
||||
if(ucharacter >= (length / sizeof(KeySequence))) return nullptr;
|
||||
if(sequences[ucharacter][0] == MachineTypes::MappedKeyboardMachine::KeyNotMapped) return nullptr;
|
||||
return sequences[ucharacter];
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,15 @@ class CharacterMapper {
|
||||
typedef uint16_t KeySequence[16];
|
||||
|
||||
/*!
|
||||
Provided in the base class as a convenience: given the lookup table of key sequences @c sequences,
|
||||
with @c length entries, returns the sequence for character @c character if it exists; otherwise
|
||||
returns @c nullptr.
|
||||
Provided in the base class as a convenience: given the C array of key sequences @c sequences,
|
||||
returns the sequence for character @c character if it exists; otherwise returns @c nullptr.
|
||||
*/
|
||||
const uint16_t *table_lookup_sequence_for_character(const KeySequence *sequences, std::size_t length, char character) const;
|
||||
template <typename Collection> const uint16_t *table_lookup_sequence_for_character(const Collection &sequences, char character) const {
|
||||
std::size_t ucharacter = size_t((unsigned char)character);
|
||||
if(ucharacter >= sizeof(sequences) / sizeof(KeySequence)) return nullptr;
|
||||
if(sequences[ucharacter][0] == MachineTypes::MappedKeyboardMachine::KeyNotMapped) return nullptr;
|
||||
return sequences[ucharacter];
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "Sizes.hpp"
|
||||
|
||||
namespace Numeric {
|
||||
|
||||
template <typename IntType> struct LSFRPolynomial {};
|
||||
@@ -67,8 +69,8 @@ template <typename IntType = uint64_t, IntType polynomial = LSFRPolynomial<IntTy
|
||||
determining the bit that was just shifted out.
|
||||
*/
|
||||
IntType next() {
|
||||
const auto result = value_ & 1;
|
||||
value_ = (value_ >> 1) ^ (result * polynomial);
|
||||
const auto result = IntType(value_ & 1);
|
||||
value_ = IntType((value_ >> 1) ^ (result * polynomial));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -76,6 +78,8 @@ template <typename IntType = uint64_t, IntType polynomial = LSFRPolynomial<IntTy
|
||||
IntType value_ = 0;
|
||||
};
|
||||
|
||||
template <uint64_t polynomial> class LFSRv: public LFSR<typename MinIntTypeValue<polynomial>::type, polynomial> {};
|
||||
|
||||
}
|
||||
|
||||
#endif /* LFSR_h */
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#define Sizes_h
|
||||
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
|
||||
/*!
|
||||
Maps to the smallest integral type that can contain max_value, from the following options:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1200"
|
||||
LastUpgradeVersion = "1240"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -56,6 +56,18 @@
|
||||
argument = "/Users/thomasharte/Downloads/test-dsk-for-rw-and-50-60-hz/TEST-RW-60Hz.DSK"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = ""/Users/thomasharte/Desktop/Soft/Apple II/WOZs/Prince of Persia side A.woz""
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--rompath=~/ROMs"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--has-disk-drive"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = ""/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Macintosh/MusicWorks 0.42.image""
|
||||
isEnabled = "NO">
|
||||
@@ -70,11 +82,15 @@
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--volume=0.001"
|
||||
isEnabled = "NO">
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--new=amstradcpc"
|
||||
isEnabled = "NO">
|
||||
argument = "--new=enterprise"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--basic-version=any"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = ""/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/ColecoVision/Galaxian (1983)(Atari).col""
|
||||
@@ -106,14 +122,22 @@
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--rompath=/Users/thomasharte/Projects/CLK/ROMImages"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--help"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--model=cpc6128"
|
||||
argument = "--help"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--model=mac512k"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--new=macintosh"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--use-square-pixels"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1200"
|
||||
LastUpgradeVersion = "1240"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -73,6 +73,7 @@
|
||||
debugDocumentVersioning = "YES"
|
||||
migratedStopOnEveryIssue = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUShaderValidationMode = "2"
|
||||
allowLocationSimulation = "NO">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1200"
|
||||
LastUpgradeVersion = "1240"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -11,12 +11,24 @@ import Cocoa
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
private var failedMetalCheck = false
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// Insert code here to initialize your application.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification) {
|
||||
// Insert code here to tear down your application.
|
||||
// Check for at least one Metal-capable GPU; this check
|
||||
// will become unnecessary if/when the minimum OS version
|
||||
// that this project supports reascends to 10.14.
|
||||
if (MTLCopyAllDevices().count == 0) {
|
||||
self.failedMetalCheck = true
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "This application requires a Metal-capable GPU"
|
||||
alert.addButton(withTitle: "Exit")
|
||||
alert.runModal()
|
||||
|
||||
let application = notification.object as! NSApplication
|
||||
application.terminate(self)
|
||||
}
|
||||
}
|
||||
|
||||
private var hasShownOpenDocument = false
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -13,17 +13,27 @@
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ZW7-Bw-4RP" customClass="MachinePanel" customModule="Clock_Signal" customModuleProvider="target">
|
||||
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ZW7-Bw-4RP" customClass="AppleIIOptionsPanel" customModule="Clock_Signal" customModuleProvider="target">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="80" y="150" width="200" height="61"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
|
||||
<rect key="contentRect" x="80" y="150" width="200" height="159"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1440"/>
|
||||
<view key="contentView" id="tpZ-0B-QQu">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="61"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="84"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="kDb-7g-cVx">
|
||||
<rect key="frame" x="18" y="47" width="162" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Use Square Pixels" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="h9q-Wb-em8">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="optionDidChange:" target="ZW7-Bw-4RP" id="pNS-aK-0no"/>
|
||||
</connections>
|
||||
</button>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ex3-VM-58z">
|
||||
<rect key="frame" x="18" y="17" width="165" height="25"/>
|
||||
<rect key="frame" x="17" y="16" width="167" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Colour" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="1" imageScaling="proportionallyDown" inset="2" selectedItem="gOu-dv-tre" id="u3N-Je-c2L">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@@ -40,16 +50,20 @@
|
||||
</popUpButton>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="ex3-VM-58z" secondAttribute="bottom" constant="20" id="4ZS-q5-TJL"/>
|
||||
<constraint firstAttribute="bottom" secondItem="ex3-VM-58z" secondAttribute="bottom" constant="20" symbolic="YES" id="4ZS-q5-TJL"/>
|
||||
<constraint firstItem="ex3-VM-58z" firstAttribute="leading" secondItem="tpZ-0B-QQu" secondAttribute="leading" constant="20" id="8Pj-Ns-TrJ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="kDb-7g-cVx" secondAttribute="trailing" constant="20" symbolic="YES" id="KHa-of-eY7"/>
|
||||
<constraint firstItem="kDb-7g-cVx" firstAttribute="leading" secondItem="tpZ-0B-QQu" secondAttribute="leading" constant="20" symbolic="YES" id="OcZ-Xa-394"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ex3-VM-58z" secondAttribute="trailing" constant="20" id="QWA-lY-ugz"/>
|
||||
<constraint firstItem="ex3-VM-58z" firstAttribute="top" secondItem="tpZ-0B-QQu" secondAttribute="top" constant="20" id="szw-WO-3tS"/>
|
||||
<constraint firstItem="ex3-VM-58z" firstAttribute="top" secondItem="kDb-7g-cVx" secondAttribute="bottom" constant="8" id="jDW-N8-c4V"/>
|
||||
<constraint firstItem="kDb-7g-cVx" firstAttribute="top" secondItem="tpZ-0B-QQu" secondAttribute="top" constant="20" symbolic="YES" id="wdj-PF-zxO"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="displayTypeButton" destination="ex3-VM-58z" id="lmZ-aN-lcj"/>
|
||||
<outlet property="squarePixelButton" destination="kDb-7g-cVx" id="zbx-o8-7yC"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-161" y="38.5"/>
|
||||
<point key="canvasLocation" x="-161" y="104.5"/>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -13,17 +13,17 @@
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="gsl-7V-TTU" customClass="Atari2600OptionsPanel" customModule="Clock_Signal" customModuleProvider="target">
|
||||
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="gsl-7V-TTU" customClass="Atari2600OptionsPanel" customModule="Clock_Signal" customModuleProvider="target">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="80" y="150" width="200" height="121"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1440"/>
|
||||
<view key="contentView" id="aQh-Pm-DEo">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="121"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="rQO-uD-fwn">
|
||||
<rect key="frame" x="14" y="73" width="86" height="32"/>
|
||||
<rect key="frame" x="13" y="74" width="88" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Reset" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="l3H-0m-aK0">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -33,7 +33,7 @@
|
||||
</connections>
|
||||
</button>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="3qw-C1-NYW">
|
||||
<rect key="frame" x="18" y="58" width="164" height="18"/>
|
||||
<rect key="frame" x="18" y="58" width="162" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Black and White" bezelStyle="regularSquare" imagePosition="left" inset="2" id="UP7-mf-IKo">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -43,7 +43,7 @@
|
||||
</connections>
|
||||
</button>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="Xbc-cw-Sc2">
|
||||
<rect key="frame" x="18" y="38" width="164" height="18"/>
|
||||
<rect key="frame" x="18" y="36" width="162" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Left Player Difficulty" bezelStyle="regularSquare" imagePosition="left" inset="2" id="wlJ-8s-PEh">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -53,7 +53,7 @@
|
||||
</connections>
|
||||
</button>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="kPV-Tm-TTc">
|
||||
<rect key="frame" x="18" y="18" width="164" height="18"/>
|
||||
<rect key="frame" x="18" y="14" width="162" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Right Player Difficulty" bezelStyle="regularSquare" imagePosition="left" inset="2" id="F05-cA-66S">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -63,7 +63,7 @@
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nt7-8K-xY9">
|
||||
<rect key="frame" x="100" y="73" width="86" height="32"/>
|
||||
<rect key="frame" x="99" y="74" width="88" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Select" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="8Na-Z1-EXS">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// AppleIIOptionsPanel.swift
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/06/2021.
|
||||
// Copyright 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
class AppleIIOptionsPanel: MachinePanel {
|
||||
var appleII: CSAppleII! {
|
||||
get {
|
||||
return self.machine.appleII
|
||||
}
|
||||
}
|
||||
var squarePixelsUserDefaultsKey: String {
|
||||
return prefixedUserDefaultsKey("useSquarePixels")
|
||||
}
|
||||
|
||||
@IBOutlet var squarePixelButton: NSButton!
|
||||
|
||||
@IBAction func optionDidChange(_ sender: AnyObject!) {
|
||||
let useSquarePixels = squarePixelButton.state == .on
|
||||
appleII.useSquarePixels = useSquarePixels
|
||||
|
||||
let standardUserDefaults = UserDefaults.standard
|
||||
standardUserDefaults.set(useSquarePixels, forKey: squarePixelsUserDefaultsKey)
|
||||
}
|
||||
|
||||
override func establishStoredOptions() {
|
||||
super.establishStoredOptions()
|
||||
|
||||
let standardUserDefaults = UserDefaults.standard
|
||||
let useSquarePixels = standardUserDefaults.bool(forKey: squarePixelsUserDefaultsKey)
|
||||
appleII.useSquarePixels = useSquarePixels
|
||||
squarePixelButton.state = useSquarePixels ? .on : .off
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,14 @@ class MachineDocument:
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissPanels() {
|
||||
activityPanel?.setIsVisible(false)
|
||||
activityPanel = nil
|
||||
|
||||
optionsPanel?.setIsVisible(false)
|
||||
optionsPanel = nil
|
||||
}
|
||||
|
||||
override func close() {
|
||||
// Close any dangling sheets.
|
||||
//
|
||||
@@ -105,11 +113,7 @@ class MachineDocument:
|
||||
machine?.stop()
|
||||
|
||||
// Dismiss panels.
|
||||
activityPanel?.setIsVisible(false)
|
||||
activityPanel = nil
|
||||
|
||||
optionsPanel?.setIsVisible(false)
|
||||
optionsPanel = nil
|
||||
dismissPanels()
|
||||
|
||||
// End the update cycle.
|
||||
actionLock.lock()
|
||||
@@ -133,23 +137,27 @@ class MachineDocument:
|
||||
volumeSlider.floatValue = userDefaultsVolume()
|
||||
}
|
||||
|
||||
private var missingROMs: [CSMissingROM] = []
|
||||
private var missingROMs: String = ""
|
||||
func configureAs(_ analysis: CSStaticAnalyser) {
|
||||
self.machineDescription = analysis
|
||||
|
||||
let missingROMs = NSMutableArray()
|
||||
if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) {
|
||||
self.machine = machine
|
||||
setupMachineOutput()
|
||||
setupActivityDisplay()
|
||||
machine.setVolume(userDefaultsVolume())
|
||||
} else {
|
||||
// Store the selected machine and list of missing ROMs, and
|
||||
// show the missing ROMs dialogue.
|
||||
self.missingROMs = missingROMs.map({$0 as! CSMissingROM})
|
||||
actionLock.lock()
|
||||
drawLock.lock()
|
||||
|
||||
let missingROMs = NSMutableString()
|
||||
if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) {
|
||||
setRomRequesterIsVisible(false)
|
||||
|
||||
self.machine = machine
|
||||
machine.setVolume(userDefaultsVolume())
|
||||
setupMachineOutput()
|
||||
} else {
|
||||
self.missingROMs = missingROMs as String
|
||||
requestRoms()
|
||||
}
|
||||
|
||||
actionLock.unlock()
|
||||
drawLock.unlock()
|
||||
}
|
||||
|
||||
enum InteractionMode {
|
||||
@@ -200,6 +208,9 @@ class MachineDocument:
|
||||
let aspectRatio = self.aspectRatio()
|
||||
machine.setView(scanTargetView, aspectRatio: Float(aspectRatio.width / aspectRatio.height))
|
||||
|
||||
// Get rid of all existing accessory panels.
|
||||
dismissPanels()
|
||||
|
||||
// Attach an options panel if one is available.
|
||||
if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName {
|
||||
Bundle.main.loadNibNamed(optionsPanelNibName, owner: self, topLevelObjects: nil)
|
||||
@@ -208,11 +219,10 @@ class MachineDocument:
|
||||
showOptions(self)
|
||||
}
|
||||
|
||||
machine.delegate = self
|
||||
// Create and populate an activity display if required.
|
||||
setupActivityDisplay()
|
||||
|
||||
// Callbacks from the OpenGL may come on a different thread, immediately following the .delegate set;
|
||||
// hence the full setup of the best-effort updater prior to setting self as a delegate.
|
||||
// scanTargetView.delegate = self
|
||||
machine.delegate = self
|
||||
scanTargetView.responderDelegate = self
|
||||
|
||||
// If this machine has a mouse, enable mouse capture; also indicate whether usurption
|
||||
@@ -252,7 +262,7 @@ class MachineDocument:
|
||||
let isStereo = self.machine.isStereo
|
||||
if selectedSamplingRate > 0 {
|
||||
// [Re]create the audio queue only if necessary.
|
||||
if self.audioQueue == nil || self.audioQueue.samplingRate != selectedSamplingRate {
|
||||
if self.audioQueue == nil || self.audioQueue.samplingRate != selectedSamplingRate || self.audioQueue != self.machine.audioQueue {
|
||||
self.machine.audioQueue = nil
|
||||
self.audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate), isStereo:isStereo)
|
||||
self.audioQueue.delegate = self
|
||||
@@ -280,8 +290,7 @@ class MachineDocument:
|
||||
|
||||
/// Delegate message to receive drag and drop files.
|
||||
final func scanTargetView(_ view: CSScanTargetView, didReceiveFileAt URL: URL) {
|
||||
let mediaSet = CSMediaSet(fileAt: URL)
|
||||
mediaSet.apply(to: self.machine)
|
||||
insertFile(URL)
|
||||
}
|
||||
|
||||
/// Action for the insert menu command; displays an NSOpenPanel and then segues into the same process
|
||||
@@ -292,13 +301,30 @@ class MachineDocument:
|
||||
openPanel.beginSheetModal(for: self.windowControllers[0].window!) { (response) in
|
||||
if response == .OK {
|
||||
for url in openPanel.urls {
|
||||
let mediaSet = CSMediaSet(fileAt: url)
|
||||
mediaSet.apply(to: self.machine)
|
||||
self.insertFile(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func insertFile(_ URL: URL) {
|
||||
// Try to insert media.
|
||||
let mediaSet = CSMediaSet(fileAt: URL)
|
||||
if !mediaSet.empty {
|
||||
mediaSet.apply(to: self.machine)
|
||||
return
|
||||
}
|
||||
|
||||
// Failing that see whether a new machine is required.
|
||||
// TODO.
|
||||
if let newMachine = CSStaticAnalyser(fileAt: URL) {
|
||||
machine?.stop()
|
||||
self.interactionMode = .notStarted
|
||||
self.scanTargetView.willChangeScanTargetOwner()
|
||||
configureAs(newMachine)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Input Management.
|
||||
|
||||
/// Upon a resign key, immediately releases all ongoing input mechanisms — any currently pressed keys,
|
||||
@@ -373,6 +399,10 @@ class MachineDocument:
|
||||
self.configureAs(selectedMachine)
|
||||
}
|
||||
|
||||
@IBAction func tableViewDoubleClick(_ sender: NSTableView?) {
|
||||
createMachine(nil)
|
||||
}
|
||||
|
||||
@IBAction func cancelCreateMachine(_ sender: NSButton?) {
|
||||
close()
|
||||
}
|
||||
@@ -383,23 +413,42 @@ class MachineDocument:
|
||||
@IBOutlet var romReceiverErrorField: NSTextField?
|
||||
@IBOutlet var romReceiverView: CSROMReceiverView?
|
||||
private var romRequestBaseText = ""
|
||||
|
||||
private func setRomRequesterIsVisible(_ visible : Bool) {
|
||||
if !visible && self.romRequesterPanel == nil {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.romRequesterPanel!.isVisible == visible {
|
||||
return
|
||||
}
|
||||
|
||||
if visible {
|
||||
self.windowControllers[0].window?.beginSheet(self.romRequesterPanel!, completionHandler: nil)
|
||||
} else {
|
||||
self.windowControllers[0].window?.endSheet(self.romRequesterPanel!)
|
||||
}
|
||||
}
|
||||
|
||||
func requestRoms() {
|
||||
// Don't act yet if there's no window controller yet.
|
||||
if self.windowControllers.count == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Load the ROM requester dialogue.
|
||||
Bundle.main.loadNibNamed("ROMRequester", owner: self, topLevelObjects: nil)
|
||||
self.romReceiverView!.delegate = self
|
||||
self.romRequestBaseText = romRequesterText!.stringValue
|
||||
romReceiverErrorField?.alphaValue = 0.0
|
||||
// Load the ROM requester dialogue if it's not already loaded.
|
||||
if self.romRequesterPanel == nil {
|
||||
Bundle.main.loadNibNamed("ROMRequester", owner: self, topLevelObjects: nil)
|
||||
self.romReceiverView!.delegate = self
|
||||
self.romRequestBaseText = romRequesterText!.stringValue
|
||||
romReceiverErrorField?.alphaValue = 0.0
|
||||
}
|
||||
|
||||
// Populate the current absentee list.
|
||||
populateMissingRomList()
|
||||
|
||||
// Show the thing.
|
||||
self.windowControllers[0].window?.beginSheet(self.romRequesterPanel!, completionHandler: nil)
|
||||
setRomRequesterIsVisible(true)
|
||||
}
|
||||
|
||||
@IBAction func cancelRequestROMs(_ sender: NSButton?) {
|
||||
@@ -407,89 +456,17 @@ class MachineDocument:
|
||||
}
|
||||
|
||||
func populateMissingRomList() {
|
||||
// Fill in the missing details; first build a list of all the individual
|
||||
// line items.
|
||||
var requestLines: [String] = []
|
||||
for missingROM in self.missingROMs {
|
||||
if let descriptiveName = missingROM.descriptiveName {
|
||||
requestLines.append("• " + descriptiveName)
|
||||
} else {
|
||||
requestLines.append("• " + missingROM.fileName)
|
||||
}
|
||||
}
|
||||
|
||||
// Suffix everything up to the penultimate line with a semicolon;
|
||||
// the penultimate line with a semicolon and a conjunctive; the final
|
||||
// line with a full stop.
|
||||
for x in 0 ..< requestLines.count {
|
||||
if x < requestLines.count - 2 {
|
||||
requestLines[x].append(";")
|
||||
} else if x < requestLines.count - 1 {
|
||||
requestLines[x].append("; and")
|
||||
} else {
|
||||
requestLines[x].append(".")
|
||||
}
|
||||
}
|
||||
romRequesterText!.stringValue = self.romRequestBaseText + requestLines.joined(separator: "\n")
|
||||
romRequesterText!.stringValue = self.romRequestBaseText + self.missingROMs
|
||||
}
|
||||
|
||||
func romReceiverView(_ view: CSROMReceiverView, didReceiveFileAt URL: URL) {
|
||||
// Test whether the file identified matches any of the currently missing ROMs.
|
||||
// If so then remove that ROM from the missing list and update the request screen.
|
||||
// If no ROMs are still missing, start the machine.
|
||||
do {
|
||||
let fileData = try Data(contentsOf: URL)
|
||||
var didInstallRom = false
|
||||
|
||||
// Try to match by size first, CRC second. Accept that some ROMs may have
|
||||
// some additional appended data. Arbitrarily allow them to be up to 10kb
|
||||
// too large.
|
||||
var index = 0
|
||||
for missingROM in self.missingROMs {
|
||||
if fileData.count >= missingROM.size && fileData.count < missingROM.size + 10*1024 {
|
||||
// Trim to size.
|
||||
let trimmedData = fileData[0 ..< missingROM.size]
|
||||
|
||||
// Get CRC.
|
||||
if missingROM.crc32s.contains( (trimmedData as NSData).crc32 ) {
|
||||
// This ROM matches; copy it into the application library,
|
||||
// strike it from the missing ROM list and decide how to
|
||||
// proceed.
|
||||
let fileManager = FileManager.default
|
||||
let targetPath = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent("ROMImages")
|
||||
.appendingPathComponent(missingROM.machineName)
|
||||
let targetFile = targetPath
|
||||
.appendingPathComponent(missingROM.fileName)
|
||||
|
||||
do {
|
||||
try fileManager.createDirectory(atPath: targetPath.path, withIntermediateDirectories: true, attributes: nil)
|
||||
try trimmedData.write(to: targetFile)
|
||||
} catch let error {
|
||||
showRomReceiverError(error: "Couldn't write to application support directory: \(error)")
|
||||
}
|
||||
|
||||
self.missingROMs.remove(at: index)
|
||||
didInstallRom = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
index = index + 1
|
||||
}
|
||||
|
||||
if didInstallRom {
|
||||
if self.missingROMs.count == 0 {
|
||||
self.windowControllers[0].window?.endSheet(self.romRequesterPanel!)
|
||||
configureAs(self.machineDescription!)
|
||||
} else {
|
||||
populateMissingRomList()
|
||||
}
|
||||
} else {
|
||||
showRomReceiverError(error: "Didn't recognise contents of \(URL.lastPathComponent)")
|
||||
}
|
||||
} catch let error {
|
||||
showRomReceiverError(error: "Couldn't read file at \(URL.absoluteString): \(error)")
|
||||
if CSMachine.attemptInstallROM(URL) {
|
||||
configureAs(self.machineDescription!)
|
||||
} else {
|
||||
showRomReceiverError(error: "Didn't recognise contents of \(URL.lastPathComponent)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,7 +570,7 @@ class MachineDocument:
|
||||
return super.validateUserInterfaceItem(item)
|
||||
}
|
||||
|
||||
/// Saves a screenshot of the
|
||||
/// Saves a screenshot of the machine's current display.
|
||||
@IBAction func saveScreenshot(_ sender: AnyObject!) {
|
||||
// Grab a date formatter and form a file name.
|
||||
let dateFormatter = DateFormatter()
|
||||
|
||||
@@ -592,6 +592,66 @@
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>sna</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ZX Spectrum SNA snapshot</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>z80</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ZX Spectrum Z80 snapshot</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>szx</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ZX Spectrum SZX snapshot</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
|
||||
@@ -368,7 +368,7 @@ API_AVAILABLE(macos(11.0))
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void)update {
|
||||
- (void)update {
|
||||
// Update buttons.
|
||||
for(CSGCJoystickButton *button in _buttons) {
|
||||
// This assumes that the values provided by GCDeviceButtonInput are
|
||||
@@ -379,7 +379,11 @@ API_AVAILABLE(macos(11.0))
|
||||
float val = axis.axis.value;
|
||||
val += 1;
|
||||
val /= 2;
|
||||
axis.position = val;
|
||||
if(axis.type == CSJoystickAxisTypeY) {
|
||||
axis.position = 1 - val;
|
||||
} else {
|
||||
axis.position = val;
|
||||
}
|
||||
}
|
||||
for(CSGCJoystickHat *hat in _hats) {
|
||||
// This assumes that the values provided by GCDeviceDirectionPad are
|
||||
|
||||
@@ -33,20 +33,15 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
|
||||
CSMachineKeyboardInputModeJoystick,
|
||||
};
|
||||
|
||||
@interface CSMissingROM: NSObject
|
||||
@property (nonatomic, readonly, nonnull) NSString *machineName;
|
||||
@property (nonatomic, readonly, nonnull) NSString *fileName;
|
||||
@property (nonatomic, readonly, nullable) NSString *descriptiveName;
|
||||
@property (nonatomic, readonly) NSUInteger size;
|
||||
@property (nonatomic, readonly, nonnull) NSArray<NSNumber *> *crc32s;
|
||||
@end
|
||||
|
||||
// Deliberately low; to ensure CSMachine has been declared as an @class already.
|
||||
#import "CSAtari2600.h"
|
||||
#import "CSZX8081.h"
|
||||
#import "CSAppleII.h"
|
||||
|
||||
@interface CSMachine : NSObject
|
||||
|
||||
+ (BOOL)attemptInstallROM:(nonnull NSURL *)url;
|
||||
|
||||
- (nonnull instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/*!
|
||||
@@ -56,7 +51,7 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
|
||||
@param missingROMs An array that is filled with a list of ROMs that the machine requested but which
|
||||
were not found; populated only if this `init` has failed.
|
||||
*/
|
||||
- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableArray<CSMissingROM *> *)missingROMs NS_DESIGNATED_INITIALIZER;
|
||||
- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableString *)missingROMs NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (float)idealSamplingRateFromRange:(NSRange)range;
|
||||
@property (readonly, getter=isStereo) BOOL stereo;
|
||||
@@ -109,5 +104,6 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
|
||||
// Special-case accessors; undefined behaviour if accessed for a machine not of the corresponding type.
|
||||
@property (nonatomic, readonly, nullable) CSAtari2600 *atari2600;
|
||||
@property (nonatomic, readonly, nullable) CSZX8081 *zx8081;
|
||||
@property (nonatomic, readonly, nullable) CSAppleII *appleII;
|
||||
|
||||
@end
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <bitset>
|
||||
#include <codecvt>
|
||||
#include <locale>
|
||||
|
||||
@interface CSMachine() <CSScanTargetViewDisplayLinkDelegate>
|
||||
- (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length;
|
||||
@@ -74,28 +76,6 @@ struct ActivityObserver: public Activity::Observer {
|
||||
__unsafe_unretained CSMachine *machine;
|
||||
};
|
||||
|
||||
@interface CSMissingROM (/*Mutability*/)
|
||||
@property (nonatomic, nonnull, copy) NSString *machineName;
|
||||
@property (nonatomic, nonnull, copy) NSString *fileName;
|
||||
@property (nonatomic, nullable, copy) NSString *descriptiveName;
|
||||
@property (nonatomic, readwrite) NSUInteger size;
|
||||
@property (nonatomic, copy) NSArray<NSNumber *> *crc32s;
|
||||
@end
|
||||
|
||||
@implementation CSMissingROM
|
||||
|
||||
@synthesize machineName=_machineName;
|
||||
@synthesize fileName=_fileName;
|
||||
@synthesize descriptiveName=_descriptiveName;
|
||||
@synthesize size=_size;
|
||||
@synthesize crc32s=_crc32s;
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"%@/%@, %lu bytes, CRCs: %@", _fileName, _descriptiveName, (unsigned long)_size, _crc32s];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CSMachine {
|
||||
SpeakerDelegate _speakerDelegate;
|
||||
ActivityObserver _activityObserver;
|
||||
@@ -126,35 +106,18 @@ struct ActivityObserver: public Activity::Observer {
|
||||
NSMutableArray<dispatch_block_t> *_inputEvents;
|
||||
}
|
||||
|
||||
- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result missingROMs:(inout NSMutableArray<CSMissingROM *> *)missingROMs {
|
||||
- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result missingROMs:(inout NSMutableString *)missingROMs {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
_analyser = result;
|
||||
|
||||
Machine::Error error;
|
||||
std::vector<ROMMachine::ROM> missing_roms;
|
||||
ROM::Request missing_roms;
|
||||
_machine.reset(Machine::MachineForTargets(_analyser.targets, CSROMFetcher(&missing_roms), error));
|
||||
if(!_machine) {
|
||||
for(const auto &missing_rom : missing_roms) {
|
||||
CSMissingROM *rom = [[CSMissingROM alloc] init];
|
||||
|
||||
// Copy/convert the primitive fields.
|
||||
rom.machineName = @(missing_rom.machine_name.c_str());
|
||||
rom.fileName = @(missing_rom.file_name.c_str());
|
||||
rom.descriptiveName = missing_rom.descriptive_name.empty() ? nil : @(missing_rom.descriptive_name.c_str());
|
||||
rom.size = missing_rom.size;
|
||||
|
||||
// Convert the CRC list.
|
||||
NSMutableArray<NSNumber *> *crc32s = [[NSMutableArray alloc] initWithCapacity:missing_rom.crc32s.size()];
|
||||
for(const auto &crc : missing_rom.crc32s) {
|
||||
[crc32s addObject:@(crc)];
|
||||
}
|
||||
rom.crc32s = crc32s;
|
||||
|
||||
// Add to the missing list.
|
||||
[missingROMs addObject:rom];
|
||||
}
|
||||
|
||||
std::wstring_convert<std::codecvt_utf8<wchar_t>> wstring_converter;
|
||||
const std::wstring description = missing_roms.description(0, L'•');
|
||||
[missingROMs appendString:[NSString stringWithUTF8String:wstring_converter.to_bytes(description).c_str()]];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -614,6 +577,10 @@ struct ActivityObserver: public Activity::Observer {
|
||||
return [[CSZX8081 alloc] initWithZX8081:_machine->raw_pointer() owner:self];
|
||||
}
|
||||
|
||||
- (CSAppleII *)appleII {
|
||||
return [[CSAppleII alloc] initWithAppleII:_machine->raw_pointer() owner:self];
|
||||
}
|
||||
|
||||
#pragma mark - Input device queries
|
||||
|
||||
- (BOOL)hasJoystick {
|
||||
@@ -783,4 +750,8 @@ struct ActivityObserver: public Activity::Observer {
|
||||
_timer = nil;
|
||||
}
|
||||
|
||||
+ (BOOL)attemptInstallROM:(NSURL *)url {
|
||||
return CSInstallROM(url);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,4 +8,5 @@
|
||||
|
||||
#include "ROMMachine.hpp"
|
||||
|
||||
ROMMachine::ROMFetcher CSROMFetcher(std::vector<ROMMachine::ROM> *missing_roms = nullptr);
|
||||
ROMMachine::ROMFetcher CSROMFetcher(ROM::Request *missing = nullptr);
|
||||
BOOL CSInstallROM(NSURL *);
|
||||
|
||||
@@ -11,47 +11,96 @@
|
||||
|
||||
#import "NSBundle+DataResource.h"
|
||||
#import "NSData+StdVector.h"
|
||||
#import "NSData+CRC32.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
ROMMachine::ROMFetcher CSROMFetcher(std::vector<ROMMachine::ROM> *missing_roms) {
|
||||
return [missing_roms] (const std::vector<ROMMachine::ROM> &roms) -> std::vector<std::unique_ptr<std::vector<std::uint8_t>>> {
|
||||
NSArray<NSURL *> *const supportURLs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask];
|
||||
namespace {
|
||||
|
||||
std::vector<std::unique_ptr<std::vector<std::uint8_t>>> results;
|
||||
for(const auto &rom: roms) {
|
||||
NSData *fileData;
|
||||
NSString *const subdirectory = [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:rom.machine_name.c_str()]];
|
||||
NSString *directoryFor(const ROM::Description &description) {
|
||||
return [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:description.machine_name.c_str()]];
|
||||
}
|
||||
|
||||
// Check for this file first within the application support directories.
|
||||
for(NSURL *supportURL in supportURLs) {
|
||||
NSURL *const fullURL = [[supportURL URLByAppendingPathComponent:subdirectory]
|
||||
URLByAppendingPathComponent:[NSString stringWithUTF8String:rom.file_name.c_str()]];
|
||||
fileData = [NSData dataWithContentsOfURL:fullURL];
|
||||
if(fileData) break;
|
||||
NSArray<NSURL *> *urlsFor(const ROM::Description &description, const std::string &file_name) {
|
||||
NSMutableArray<NSURL *> *const urls = [[NSMutableArray alloc] init];
|
||||
NSArray<NSURL *> *const supportURLs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask];
|
||||
NSString *const subdirectory = directoryFor(description);
|
||||
|
||||
for(NSURL *supportURL in supportURLs) {
|
||||
[urls addObject:[[supportURL URLByAppendingPathComponent:subdirectory]
|
||||
URLByAppendingPathComponent:[NSString stringWithUTF8String:file_name.c_str()]]];
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BOOL CSInstallROM(NSURL *url) {
|
||||
NSData *const data = [NSData dataWithContentsOfURL:url];
|
||||
if(!data) return NO;
|
||||
|
||||
// Try for a direct CRC match.
|
||||
std::optional<ROM::Description> target_description;
|
||||
target_description = ROM::Description::from_crc(uint32_t(data.crc32.integerValue));
|
||||
|
||||
// See whether there's an acceptable trimming that creates a CRC match.
|
||||
if(!target_description) {
|
||||
const std::vector<ROM::Description> descriptions = ROM::all_descriptions();
|
||||
for(const auto &description: descriptions) {
|
||||
if(description.size > data.length) continue;
|
||||
|
||||
NSData *const trimmedData = [data subdataWithRange:NSMakeRange(0, description.size)];
|
||||
if(description.crc32s.find(uint32_t(trimmedData.crc32.unsignedIntValue)) != description.crc32s.end()) {
|
||||
target_description = description;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Failing that, check inside the application bundle.
|
||||
if(!fileData) {
|
||||
fileData = [[NSBundle mainBundle]
|
||||
dataForResource:[NSString stringWithUTF8String:rom.file_name.c_str()]
|
||||
withExtension:nil
|
||||
subdirectory:subdirectory];
|
||||
}
|
||||
// If no destination was found, stop.
|
||||
if(!target_description) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Store an appropriate result, accumulating a list of the missing if requested.
|
||||
if(!fileData) {
|
||||
results.emplace_back(nullptr);
|
||||
// Copy the data to its destination and report success.
|
||||
NSURL *const targetURL = [urlsFor(*target_description, target_description->file_names[0]) firstObject];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:targetURL.URLByDeletingLastPathComponent.path withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
[data writeToURL:targetURL atomically:NO];
|
||||
|
||||
if(missing_roms) {
|
||||
missing_roms->push_back(rom);
|
||||
return YES;
|
||||
}
|
||||
|
||||
ROMMachine::ROMFetcher CSROMFetcher(ROM::Request *missing) {
|
||||
return [missing] (const ROM::Request &roms) -> ROM::Map {
|
||||
ROM::Map results;
|
||||
for(const auto &description: roms.all_descriptions()) {
|
||||
for(const auto &file_name: description.file_names) {
|
||||
NSData *fileData;
|
||||
|
||||
// Check for this file first within the application support directories.
|
||||
for(NSURL *fileURL in urlsFor(description, file_name)) {
|
||||
fileData = [NSData dataWithContentsOfURL:fileURL];
|
||||
if(fileData) break;
|
||||
}
|
||||
|
||||
// Failing that, check inside the application bundle.
|
||||
if(!fileData) {
|
||||
fileData = [[NSBundle mainBundle]
|
||||
dataForResource:[NSString stringWithUTF8String:file_name.c_str()]
|
||||
withExtension:nil
|
||||
subdirectory:directoryFor(description)];
|
||||
}
|
||||
|
||||
// Store an appropriate result.
|
||||
if(fileData) {
|
||||
results[description.name] = fileData.stdVector8;
|
||||
}
|
||||
}
|
||||
else {
|
||||
auto data = std::make_unique<std::vector<std::uint8_t>>();
|
||||
*data = fileData.stdVector8;
|
||||
results.emplace_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
if(missing) {
|
||||
*missing = roms.subtract(results);
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
@@ -41,6 +41,35 @@ typedef NS_ENUM(NSInteger, CSMachineCPCModel) {
|
||||
CSMachineCPCModel6128
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineEnterpriseModel) {
|
||||
CSMachineEnterpriseModel64,
|
||||
CSMachineEnterpriseModel128,
|
||||
CSMachineEnterpriseModel256,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineEnterpriseSpeed) {
|
||||
CSMachineEnterpriseSpeed4MHz,
|
||||
CSMachineEnterpriseSpeed6MHz
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineEnterpriseEXOS) {
|
||||
CSMachineEnterpriseEXOSVersion21,
|
||||
CSMachineEnterpriseEXOSVersion20,
|
||||
CSMachineEnterpriseEXOSVersion10,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineEnterpriseBASIC) {
|
||||
CSMachineEnterpriseBASICVersion21,
|
||||
CSMachineEnterpriseBASICVersion11,
|
||||
CSMachineEnterpriseBASICVersion10,
|
||||
CSMachineEnterpriseBASICNone,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineEnterpriseDOS) {
|
||||
CSMachineEnterpriseDOSEXDOS,
|
||||
CSMachineEnterpriseDOSNone,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineMacintoshModel) {
|
||||
CSMachineMacintoshModel128k,
|
||||
CSMachineMacintoshModel512k,
|
||||
@@ -63,6 +92,10 @@ typedef NS_ENUM(NSInteger, CSMachineOricDiskInterface) {
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineSpectrumModel) {
|
||||
CSMachineSpectrumModelSixteenK,
|
||||
CSMachineSpectrumModelFortyEightK,
|
||||
CSMachineSpectrumModelOneTwoEightK,
|
||||
CSMachineSpectrumModelPlus2,
|
||||
CSMachineSpectrumModelPlus2a,
|
||||
CSMachineSpectrumModelPlus3,
|
||||
};
|
||||
@@ -92,6 +125,7 @@ typedef int Kilobytes;
|
||||
- (instancetype)initWithAppleIIgsModel:(CSMachineAppleIIgsModel)model memorySize:(Kilobytes)memorySize;
|
||||
- (instancetype)initWithAtariSTModel:(CSMachineAtariSTModel)model;
|
||||
- (instancetype)initWithElectronDFS:(BOOL)dfs adfs:(BOOL)adfs ap6:(BOOL)ap6 sidewaysRAM:(BOOL)sidewaysRAM;
|
||||
- (instancetype)initWithEnterpriseModel:(CSMachineEnterpriseModel)model speed:(CSMachineEnterpriseSpeed)speed exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion dos:(CSMachineEnterpriseDOS)dos;
|
||||
- (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model;
|
||||
- (instancetype)initWithMSXRegion:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive;
|
||||
- (instancetype)initWithOricModel:(CSMachineOricModel)model diskInterface:(CSMachineOricDiskInterface)diskInterface;
|
||||
@@ -110,6 +144,8 @@ typedef int Kilobytes;
|
||||
- (instancetype)initWithFileAtURL:(NSURL *)url;
|
||||
- (void)applyToMachine:(CSMachine *)machine;
|
||||
|
||||
@property(nonatomic, readonly) BOOL empty;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "../../../../../Analyser/Static/AppleIIgs/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/AtariST/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/Commodore/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/Enterprise/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/Macintosh/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/MSX/Target.hpp"
|
||||
#include "../../../../../Analyser/Static/Oric/Target.hpp"
|
||||
@@ -130,6 +131,51 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithEnterpriseModel:(CSMachineEnterpriseModel)model speed:(CSMachineEnterpriseSpeed)speed exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion dos:(CSMachineEnterpriseDOS)dos {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
using Target = Analyser::Static::Enterprise::Target;
|
||||
auto target = std::make_unique<Target>();
|
||||
|
||||
switch(model) {
|
||||
case CSMachineEnterpriseModel64: target->model = Target::Model::Enterprise64; break;
|
||||
default:
|
||||
case CSMachineEnterpriseModel128: target->model = Target::Model::Enterprise128; break;
|
||||
case CSMachineEnterpriseModel256: target->model = Target::Model::Enterprise256; break;
|
||||
}
|
||||
|
||||
switch(speed) {
|
||||
case CSMachineEnterpriseSpeed6MHz: target->speed = Target::Speed::SixMHz; break;
|
||||
default:
|
||||
case CSMachineEnterpriseSpeed4MHz: target->speed = Target::Speed::FourMHz; break;
|
||||
}
|
||||
|
||||
switch(exosVersion) {
|
||||
case CSMachineEnterpriseEXOSVersion21: target->exos_version = Target::EXOSVersion::v21; break;
|
||||
default:
|
||||
case CSMachineEnterpriseEXOSVersion20: target->exos_version = Target::EXOSVersion::v20; break;
|
||||
case CSMachineEnterpriseEXOSVersion10: target->exos_version = Target::EXOSVersion::v10; break;
|
||||
}
|
||||
|
||||
switch(basicVersion) {
|
||||
case CSMachineEnterpriseBASICNone: target->basic_version = Target::BASICVersion::None; break;
|
||||
default:
|
||||
case CSMachineEnterpriseBASICVersion21: target->basic_version = Target::BASICVersion::v21; break;
|
||||
case CSMachineEnterpriseBASICVersion11: target->basic_version = Target::BASICVersion::v11; break;
|
||||
case CSMachineEnterpriseBASICVersion10: target->basic_version = Target::BASICVersion::v10; break;
|
||||
}
|
||||
|
||||
switch(dos) {
|
||||
case CSMachineEnterpriseDOSEXDOS: target->dos = Target::DOS::EXDOS; break;
|
||||
default:
|
||||
case CSMachineEnterpriseDOSNone: target->dos = Target::DOS::None; break;
|
||||
}
|
||||
|
||||
_targets.push_back(std::move(target));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
@@ -194,8 +240,12 @@
|
||||
using Target = Analyser::Static::ZXSpectrum::Target;
|
||||
auto target = std::make_unique<Target>();
|
||||
switch(model) {
|
||||
case CSMachineSpectrumModelPlus2a: target->model = Target::Model::Plus2a; break;
|
||||
case CSMachineSpectrumModelPlus3: target->model = Target::Model::Plus3; break;
|
||||
case CSMachineSpectrumModelSixteenK: target->model = Target::Model::SixteenK; break;
|
||||
case CSMachineSpectrumModelFortyEightK: target->model = Target::Model::FortyEightK; break;
|
||||
case CSMachineSpectrumModelOneTwoEightK: target->model = Target::Model::OneTwoEightK; break;
|
||||
case CSMachineSpectrumModelPlus2: target->model = Target::Model::Plus2; break;
|
||||
case CSMachineSpectrumModelPlus2a: target->model = Target::Model::Plus2a; break;
|
||||
case CSMachineSpectrumModelPlus3: target->model = Target::Model::Plus3; break;
|
||||
}
|
||||
_targets.push_back(std::move(target));
|
||||
}
|
||||
@@ -272,6 +322,7 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
|
||||
case Analyser::Machine::AtariST: return @"CompositeOptions";
|
||||
case Analyser::Machine::ColecoVision: return @"CompositeOptions";
|
||||
case Analyser::Machine::Electron: return @"QuickLoadCompositeOptions";
|
||||
case Analyser::Machine::Enterprise: return @"CompositeOptions";
|
||||
case Analyser::Machine::Macintosh: return @"MacintoshOptions";
|
||||
case Analyser::Machine::MasterSystem: return @"CompositeOptions";
|
||||
case Analyser::Machine::MSX: return @"QuickLoadCompositeOptions";
|
||||
@@ -305,4 +356,8 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
|
||||
[machine applyMedia:_media];
|
||||
}
|
||||
|
||||
- (BOOL)empty {
|
||||
return _media.empty();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
18
OSBindings/Mac/Clock Signal/Machine/Wrappers/CSAppleII.h
Normal file
18
OSBindings/Mac/Clock Signal/Machine/Wrappers/CSAppleII.h
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// CSAppleII.h
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/06/2021.
|
||||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
@class CSAppleII;
|
||||
#import "CSMachine.h"
|
||||
|
||||
@interface CSAppleII : NSObject
|
||||
|
||||
- (instancetype)initWithAppleII:(void *)appleII owner:(CSMachine *)machine;
|
||||
|
||||
@property (nonatomic, assign) BOOL useSquarePixels;
|
||||
|
||||
@end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user