mirror of
https://github.com/TomHarte/CLK.git
synced 2026-04-26 19:17:52 +00:00
Compare commits
385 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4be5ee5b35 | |||
| 92e6dc64d4 | |||
| a9d945d6d2 | |||
| 5e465f1ff4 | |||
| 5359964fef | |||
| fa8be26f9f | |||
| aabfe7c284 | |||
| d011b10b5d | |||
| 332b37063f | |||
| b3a9e39be3 | |||
| 67590cf06b | |||
| 236fdacb36 | |||
| f422cda553 | |||
| 2c44d3a7d3 | |||
| 051ce98ecb | |||
| 33ae24c961 | |||
| 4247d0ef40 | |||
| ffababdb45 | |||
| 176bda9eb8 | |||
| 9f0a0443a8 | |||
| fd1a7e78c5 | |||
| 909fa57b27 | |||
| 5630b1c351 | |||
| c4fe38a61f | |||
| 5b4f303e35 | |||
| c9c1bde6e2 | |||
| d01e1f3bb1 | |||
| fd32e63459 | |||
| dbbb1d60fc | |||
| 1ce013bcf7 | |||
| 86bf019aac | |||
| d00546dd77 | |||
| cf33e17688 | |||
| c5c6c5ff72 | |||
| fa0835abd8 | |||
| f232b179ed | |||
| a4a0026cab | |||
| eac7493180 | |||
| 989fb32fba | |||
| 735afcfabb | |||
| 37152a1fad | |||
| 4e86184955 | |||
| d23dbb96c2 | |||
| 4586e4b4c1 | |||
| de5cdbf18c | |||
| 8c2294fc0d | |||
| b0b82782ad | |||
| b9f5802c89 | |||
| 29235f1276 | |||
| 8c74e2a323 | |||
| ae2936b9c3 | |||
| 0d295a6338 | |||
| 3ebd6c6871 | |||
| 6e2cd0ace6 | |||
| af82a0bcda | |||
| 6fe208ae77 | |||
| f569b86c90 | |||
| b622cc9536 | |||
| 7dfd5ea0d0 | |||
| a81309433c | |||
| 902f388cb1 | |||
| 0cc5a9d74f | |||
| 5e98e6502d | |||
| fe7a206fc5 | |||
| c5704aaaff | |||
| e115f09f51 | |||
| 32cd142629 | |||
| b00be303aa | |||
| 273e23bd98 | |||
| 5063e6943d | |||
| ce32747973 | |||
| a8ef8dfb21 | |||
| 7658edca62 | |||
| 056028e07b | |||
| 34992126a8 | |||
| d30d4e8f89 | |||
| f562deca48 | |||
| b2b7aa221b | |||
| b98a9a8487 | |||
| 7f36a8a746 | |||
| 2fe6e9c7fc | |||
| 62919e77d4 | |||
| 871b724290 | |||
| ca23c04ba1 | |||
| 44b8f75611 | |||
| 301df785fe | |||
| 9657109471 | |||
| 139569e291 | |||
| 1383c1dad4 | |||
| 1d2cdd85a3 | |||
| 76082b1271 | |||
| 25dcbf918d | |||
| 6c72c1842b | |||
| 4e4388dc35 | |||
| 54bff80ecc | |||
| 78073aaa11 | |||
| 4df01a7e0d | |||
| f4d15d0640 | |||
| 64842d4de2 | |||
| f52315ac92 | |||
| cc88877109 | |||
| d7568e57c3 | |||
| d49b301fca | |||
| 0113bcbea7 | |||
| 0332bb4f12 | |||
| 82a5d5116a | |||
| 62a6797ef3 | |||
| bb66033682 | |||
| d4aa0799a9 | |||
| fba2d37714 | |||
| e891697f88 | |||
| ee6ac3b4a9 | |||
| c8130f9d6f | |||
| 8abd837c8b | |||
| 02ad080bb8 | |||
| 5887e3e580 | |||
| 1994b2dc9f | |||
| d7a82d00b1 | |||
| 0017bd6d0f | |||
| 15f30995b1 | |||
| 37ca0e4f81 | |||
| e400aa200c | |||
| e168298aa0 | |||
| c4afbf8f2e | |||
| 112aff9887 | |||
| a8207ded4f | |||
| bafef023a5 | |||
| 9b39eebc2d | |||
| 02fdcd6ece | |||
| 49601e0a78 | |||
| cf10abff5b | |||
| e75c27cb66 | |||
| d27f0e3633 | |||
| e19bd0d517 | |||
| 3427120b3f | |||
| ecc623cd6c | |||
| 3ef615f508 | |||
| 1c4c3a6cae | |||
| fd7142f6a1 | |||
| b30fda3c36 | |||
| 7e43b40415 | |||
| 53501a9443 | |||
| c5dc65fc61 | |||
| b389889e1f | |||
| 02a29056b7 | |||
| 5e789b4b0c | |||
| 9ff09a45be | |||
| fc02a3d34b | |||
| 48f983c040 | |||
| dd25d387fe | |||
| 80a503f317 | |||
| 5aa9168dd6 | |||
| b3f01fe314 | |||
| e688d87c22 | |||
| 332fb2f384 | |||
| 5332bcd6b4 | |||
| 55c59e6164 | |||
| 80bfa1859f | |||
| 58e1880773 | |||
| c8c4c99f09 | |||
| 350f424055 | |||
| bc5b7a6725 | |||
| 7b4e71e6dd | |||
| a32202ab72 | |||
| 193c027c8b | |||
| 3dd07b6ac1 | |||
| 12d912b627 | |||
| f847a72696 | |||
| 967c3f6dba | |||
| 6cf87e3aa9 | |||
| cbc6477431 | |||
| 45d0f101a7 | |||
| cba96aee37 | |||
| 17325834b5 | |||
| 3673144a44 | |||
| 4df49d9f18 | |||
| 8b04608d68 | |||
| 2bac276870 | |||
| 213f9850e7 | |||
| 378bffbf84 | |||
| c291d5313d | |||
| 1f6f665639 | |||
| e81233c586 | |||
| b946029394 | |||
| 6095936354 | |||
| fe79a1231d | |||
| 76a5872d17 | |||
| 0d72c75e15 | |||
| 2e0e89c494 | |||
| 7d6b7a5874 | |||
| 48f8ddf53a | |||
| 9aae07b737 | |||
| d7abdc8017 | |||
| cb81156835 | |||
| 1fd8d94e2e | |||
| e4fe127444 | |||
| aeabd5f113 | |||
| 58f7d4065c | |||
| 60f25a3ba4 | |||
| d267571dc6 | |||
| 3c34aa6696 | |||
| 5dc00a2092 | |||
| b20d489bf0 | |||
| df39870587 | |||
| f742eab4be | |||
| e9c8c61dcf | |||
| e5f09002e9 | |||
| d42f005e17 | |||
| 24e060abee | |||
| 8b6d763442 | |||
| e239745f63 | |||
| cfef2b4e19 | |||
| cf93c39881 | |||
| 5d223bce4c | |||
| b454ebc1c9 | |||
| 7cf9910cae | |||
| 79ab1d8cb1 | |||
| 7cd20f5d12 | |||
| 5396d751e1 | |||
| d23e715650 | |||
| 0791bce338 | |||
| 2bcb74072a | |||
| c5f2f17f33 | |||
| 62a8bf4261 | |||
| ebda18b44e | |||
| a8f41b9017 | |||
| 410c19a7da | |||
| a346e2e04b | |||
| 2d114b6677 | |||
| 02e74ca1f4 | |||
| 69122cdec4 | |||
| d730168631 | |||
| 2f210ebe3b | |||
| 693b53baa2 | |||
| 77554879a5 | |||
| 45363922b5 | |||
| 0463c1ceda | |||
| b35a55a658 | |||
| 4da68c9fa8 | |||
| 72f133f31b | |||
| af4a8f6d9c | |||
| b5899a2e42 | |||
| 4ee8f8564e | |||
| ff08c03bc5 | |||
| 95dd430b0d | |||
| 20eb8b1442 | |||
| 2d6a0b3ed0 | |||
| 80f0ce78e0 | |||
| fde0e2434e | |||
| fe2da7fd95 | |||
| 25e783ff2f | |||
| 2eb94f1b66 | |||
| 2cdf6ac8f9 | |||
| 309c58a93d | |||
| 700bd0ddd4 | |||
| bd5a2f240d | |||
| 73054d971c | |||
| 8c7f2491d7 | |||
| 24fcbea6f2 | |||
| fddc9c8c48 | |||
| 294893b7da | |||
| 564542420b | |||
| 3f7e3e6d75 | |||
| 6521d7d02b | |||
| ad162a4e4a | |||
| 676b1f6fdc | |||
| 406ef4e16c | |||
| 217976350b | |||
| e8f860d6fe | |||
| 859e6e2396 | |||
| 51186e615f | |||
| bd8287fda3 | |||
| 287ff99bbc | |||
| 0bbfcedabb | |||
| 812e1e637d | |||
| f20fd38940 | |||
| b4cfabc005 | |||
| c49e160501 | |||
| a0a24902d5 | |||
| 1047bc8a80 | |||
| 0eed49c4cb | |||
| e7f09e2ece | |||
| 89678f1ea7 | |||
| e43ec7d549 | |||
| 95395132f0 | |||
| 89293d8481 | |||
| e6de24557f | |||
| 66d76dc36a | |||
| 06629def62 | |||
| 97aeb5e930 | |||
| bf45b6e20b | |||
| 6ad41326b0 | |||
| 2bbca3c169 | |||
| ae903b0712 | |||
| a2a7f82716 | |||
| 00456c891a | |||
| afd5faaab1 | |||
| bb33cf0f8d | |||
| edc510572a | |||
| bc6cffa95c | |||
| 48ed2912b0 | |||
| a8af262c41 | |||
| dcf49933bc | |||
| 9c014001da | |||
| 4f410088dd | |||
| e1c1b66dc5 | |||
| 23c3a1fa99 | |||
| ef6e1b2f74 | |||
| e130ae0a8a | |||
| 1a1e3281e4 | |||
| a4e55c9362 | |||
| 0b4c51eebd | |||
| 1107f0d9a3 | |||
| 775819432b | |||
| a71a60937f | |||
| 5e661fe96b | |||
| a9f5b17fcb | |||
| b0c2b55fc9 | |||
| 925832aac5 | |||
| 994131e2ea | |||
| f8d27d0ae0 | |||
| fc50af0e17 | |||
| 087d3535f6 | |||
| e9d310962f | |||
| 0f9c89d259 | |||
| 258c37685b | |||
| 56f092a0c3 | |||
| 6c3048ffbf | |||
| c58eba61de | |||
| 8a54773f1b | |||
| 2c483e7b97 | |||
| 1027e9ffdc | |||
| 85d6957e03 | |||
| c3609b66a9 | |||
| 605f4a92d7 | |||
| d395e2bc75 | |||
| e6ccdc5a97 | |||
| a68c7aa45f | |||
| 66e959ab65 | |||
| d68b172a40 | |||
| d3ee778265 | |||
| da96df7df7 | |||
| 4ea82581ec | |||
| 4473d3400e | |||
| 2f1f843e48 | |||
| 53a3d9042e | |||
| 6eb32f98b2 | |||
| 0fad97ed48 | |||
| 27246247a2 | |||
| cbc96e2223 | |||
| 8fdf32cde8 | |||
| 03a94e59e2 | |||
| 2c0610fef8 | |||
| 60b3c51085 | |||
| d7b5a45417 | |||
| e11060bde8 | |||
| 4653de9161 | |||
| 1926ad9215 | |||
| 33d047c703 | |||
| fadda00246 | |||
| a3fed788d8 | |||
| dde31e8687 | |||
| 190fb009bc | |||
| 62574d04c6 | |||
| 2496257bcf | |||
| ab73b4de6b | |||
| 6c1c32baca | |||
| 239cc15c8f | |||
| 6b437c3907 | |||
| 4756f63169 | |||
| 7229acb34f | |||
| 43cb91760a | |||
| 7bb4d052d1 | |||
| 5885bdf0f8 | |||
| 05042b1859 | |||
| 3ca2f72184 | |||
| b076450b73 | |||
| 7a90662c06 | |||
| eb31aaeb7d | |||
| ebfb215246 | |||
| eb97e4e518 | |||
| 61d3e65c05 | |||
| 0ac5681d13 | |||
| 1e27c5759b | |||
| 5e71aedc99 |
@@ -44,7 +44,7 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetDFSCatalogue(const std::s
|
||||
case 3: catalogue->bootOption = Catalogue::BootOption::ExecBOOT; break;
|
||||
}
|
||||
|
||||
for(std::size_t file_offset = 8; file_offset < final_file_offset; file_offset += 8) {
|
||||
for(std::size_t file_offset = 8; file_offset <= final_file_offset; file_offset += 8) {
|
||||
File new_file;
|
||||
char name[10];
|
||||
snprintf(name, 10, "%c.%.7s", names->samples[0][file_offset + 7] & 0x7f, &names->samples[0][file_offset]);
|
||||
|
||||
@@ -85,6 +85,12 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
|
||||
TargetPlatform::IntType,
|
||||
bool
|
||||
) {
|
||||
const auto early_exit = [](auto &ptr) {
|
||||
TargetList list;
|
||||
list.push_back(std::move(ptr));
|
||||
return list;
|
||||
};
|
||||
|
||||
auto targetElectron = std::make_unique<ElectronTarget>();
|
||||
auto targetBBC = std::make_unique<BBCMicroTarget>();
|
||||
auto targetArchimedes = std::make_unique<ArchimedesTarget>();
|
||||
@@ -126,7 +132,7 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
|
||||
if(dfs_catalogue || (adfs_catalogue && !adfs_catalogue->has_large_sectors && adfs_catalogue->is_hugo)) {
|
||||
// Accept the disk and determine whether DFS or ADFS ROMs are implied.
|
||||
|
||||
// Electron: use the Pres ADFS if using an ADFS, as it leaves Page at &EOO.
|
||||
// Electron: use the Pres ADFS if using an ADFS, as it leaves Page at &E00.
|
||||
targetElectron->media.disks = media.disks;
|
||||
targetElectron->has_dfs = bool(dfs_catalogue);
|
||||
targetElectron->has_pres_adfs = bool(adfs_catalogue);
|
||||
@@ -136,6 +142,14 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
|
||||
targetBBC->has_1770dfs = bool(dfs_catalogue);
|
||||
targetBBC->has_adfs = bool(adfs_catalogue);
|
||||
|
||||
// Special case: if there's only one file, and it is called CPMDISC,
|
||||
// select a BBC with the Z80 second processor.
|
||||
const auto &files = dfs_catalogue ? dfs_catalogue->files : adfs_catalogue->files;
|
||||
if(files.size() == 1 && files[0].name == "$.CPMDISC") {
|
||||
targetBBC->tube_processor = BBCMicroTarget::TubeProcessor::Z80;
|
||||
return early_exit(targetBBC);
|
||||
}
|
||||
|
||||
// Check whether a simple shift+break will do for loading this disk.
|
||||
const auto bootOption = (dfs_catalogue ?: adfs_catalogue)->bootOption;
|
||||
if(bootOption != Catalogue::BootOption::None) {
|
||||
@@ -160,10 +174,19 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
|
||||
sole_basic_file ? "CHAIN \"" + sole_basic_file->name + "\"\n" : "*CAT\n";
|
||||
}
|
||||
|
||||
// Further special case: if any of the files have a top word of 0x0003 then
|
||||
// they're for a 6502 second processor, so provide a BBC with one of those.
|
||||
for(const auto &file: files) {
|
||||
if((file.load_address >> 16) == 3) {
|
||||
targetBBC->tube_processor = BBCMicroTarget::TubeProcessor::WDC65C02;
|
||||
return early_exit(targetBBC);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a slight preference for the BBC over the Electron, all else being equal, if this is a DFS floppy.
|
||||
format_prefers_bbc = bool(dfs_catalogue);
|
||||
|
||||
for(const auto &file: dfs_catalogue ? dfs_catalogue->files : adfs_catalogue->files) {
|
||||
for(const auto &file: files) {
|
||||
// Electron: check whether adding the AP6 ROM is justified.
|
||||
// For now this is an incredibly dense text search;
|
||||
// if any of the commands that aren't usually present
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "Reflection/Enum.hpp"
|
||||
#include "Reflection/Struct.hpp"
|
||||
#include <string>
|
||||
|
||||
@@ -43,6 +44,10 @@ struct BBCMicroTarget: public ::Analyser::Static::Target, public Reflection::Str
|
||||
bool has_1770dfs = false;
|
||||
bool has_adfs = false;
|
||||
bool has_sideways_ram = true;
|
||||
bool has_beebsid = false;
|
||||
|
||||
ReflectableEnum(TubeProcessor, None, WDC65C02, Z80);
|
||||
TubeProcessor tube_processor = TubeProcessor::None;
|
||||
|
||||
BBCMicroTarget() : Analyser::Static::Target(Machine::BBCMicro) {}
|
||||
|
||||
@@ -52,6 +57,9 @@ private:
|
||||
DeclareField(has_1770dfs);
|
||||
DeclareField(has_adfs);
|
||||
DeclareField(has_sideways_ram);
|
||||
DeclareField(has_beebsid);
|
||||
AnnounceEnum(TubeProcessor);
|
||||
DeclareField(tube_processor);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,9 +16,18 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string rtrimmed(const std::string &input) {
|
||||
auto trimmed = input;
|
||||
trimmed.erase(std::find_if(trimmed.rbegin(), trimmed.rend(), [](const char ch) {
|
||||
return !std::isspace(ch);
|
||||
}).base(), trimmed.end());
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
bool strcmp_insensitive(const char *a, const char *b) {
|
||||
if(std::strlen(a) != std::strlen(b)) return false;
|
||||
while(*a) {
|
||||
@@ -104,58 +113,85 @@ void InspectCatalogue(
|
||||
return;
|
||||
}
|
||||
|
||||
// If only one file is [potentially] BASIC, run that one; otherwise if only one has a suffix
|
||||
// that AMSDOS allows to be omitted, pick that one.
|
||||
int basic_files = 0;
|
||||
int implicit_suffixed_files = 0;
|
||||
const auto run_name = [&]() -> std::optional<std::string> {
|
||||
// Collect:
|
||||
//
|
||||
// 1. a set of all files that can be run without specifying an extension plus their appearance counts;
|
||||
// 2. a set of all BASIC file names.
|
||||
std::unordered_map<std::string, int> candidates;
|
||||
std::unordered_set<std::string> basic_names;
|
||||
for(std::size_t c = 0; c < candidate_files.size(); c++) {
|
||||
// Files with nothing but spaces in their name can't be loaded by the user, so disregard them.
|
||||
if(
|
||||
(candidate_files[c]->type == " " && candidate_files[c]->name == " ") ||
|
||||
!is_implied_extension(candidate_files[c]->type)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::size_t last_basic_file = 0;
|
||||
std::size_t last_implicit_suffixed_file = 0;
|
||||
|
||||
for(std::size_t c = 0; c < candidate_files.size(); c++) {
|
||||
// Files with nothing but spaces in their name can't be loaded by the user, so disregard them.
|
||||
if(candidate_files[c]->type == " " && candidate_files[c]->name == " ")
|
||||
continue;
|
||||
|
||||
// Check for whether this is [potentially] BASIC.
|
||||
if(candidate_files[c]->data.size() >= 128 && !((candidate_files[c]->data[18] >> 1) & 7)) {
|
||||
basic_files++;
|
||||
last_basic_file = c;
|
||||
}
|
||||
|
||||
// Check suffix for emptiness.
|
||||
if(is_implied_extension(candidate_files[c]->type)) {
|
||||
implicit_suffixed_files++;
|
||||
last_implicit_suffixed_file = c;
|
||||
}
|
||||
}
|
||||
if(basic_files == 1 || implicit_suffixed_files == 1) {
|
||||
std::size_t selected_file = (basic_files == 1) ? last_basic_file : last_implicit_suffixed_file;
|
||||
target->loading_command = RunCommandFor(*candidate_files[selected_file]);
|
||||
return;
|
||||
}
|
||||
|
||||
// One more guess: if only one remaining candidate file has a different name than the others,
|
||||
// assume it is intended to stand out.
|
||||
std::map<std::string, int> name_counts;
|
||||
std::map<std::string, std::size_t> indices_by_name;
|
||||
std::size_t index = 0;
|
||||
for(const auto &file : candidate_files) {
|
||||
name_counts[file->name]++;
|
||||
indices_by_name[file->name] = index;
|
||||
index++;
|
||||
}
|
||||
if(name_counts.size() == 2) {
|
||||
for(const auto &pair : name_counts) {
|
||||
if(pair.second == 1) {
|
||||
target->loading_command = RunCommandFor(*candidate_files[indices_by_name[pair.first]]);
|
||||
return;
|
||||
++candidates[candidate_files[c]->name];
|
||||
if(candidate_files[c]->data.size() >= 128 && !((candidate_files[c]->data[18] >> 1) & 7)) {
|
||||
basic_names.insert(candidate_files[c]->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Desperation.
|
||||
target->loading_command = "cat\n";
|
||||
// Only one candidate total.
|
||||
if(candidates.size() == 1) {
|
||||
return candidates.begin()->first;
|
||||
}
|
||||
|
||||
// Only one BASIC candidate.
|
||||
if(basic_names.size() == 1) {
|
||||
return *basic_names.begin();
|
||||
}
|
||||
|
||||
// Exactly two candidate names, but only one is a unique name.
|
||||
if(candidates.size() == 2) {
|
||||
const auto item1 = candidates.begin();
|
||||
const auto item2 = std::next(item1);
|
||||
|
||||
if(item1->second == 1 && item2->second != 1) {
|
||||
return item1->first;
|
||||
}
|
||||
if(item2->second == 1 && item1->second != 1) {
|
||||
return item2->first;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from candidates anything that is just a suffixed version of
|
||||
// another name, as long as the other name is three or more characters.
|
||||
std::vector<std::string> to_remove;
|
||||
for(const auto &lhs: candidates) {
|
||||
const auto trimmed = rtrimmed(lhs.first);
|
||||
if(trimmed.size() < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(const auto &rhs: candidates) {
|
||||
if(lhs.first == rhs.first) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(rhs.first.find(trimmed) == 0) {
|
||||
to_remove.push_back(rhs.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
for(const auto &candidate: to_remove) {
|
||||
candidates.erase(candidate);
|
||||
}
|
||||
if(candidates.size() == 1) {
|
||||
return candidates.begin()->first;
|
||||
}
|
||||
|
||||
return {};
|
||||
} ();
|
||||
|
||||
if(run_name.has_value()) {
|
||||
target->loading_command = "run\"" + rtrimmed(*run_name) + "\n";
|
||||
} else {
|
||||
target->loading_command = "cat\n";
|
||||
}
|
||||
}
|
||||
|
||||
bool CheckBootSector(
|
||||
|
||||
@@ -325,7 +325,7 @@ public:
|
||||
/// Flushes all accumulated time.
|
||||
inline void flush() {
|
||||
if(!is_flushed_) {
|
||||
task_queue_.flush();
|
||||
task_queue_.lock_flush();
|
||||
object_.run_for(time_since_update_.template flush<TargetTimeScale>());
|
||||
is_flushed_ = true;
|
||||
}
|
||||
|
||||
+30
-17
@@ -8,8 +8,8 @@
|
||||
|
||||
#include "1770.hpp"
|
||||
|
||||
#include "Storage/Disk/Encodings/MFM/Constants.hpp"
|
||||
#include "Outputs/Log.hpp"
|
||||
#include "Storage/Disk/Encodings/MFM/Constants.hpp"
|
||||
|
||||
namespace {
|
||||
using Logger = Log::Logger<Log::Source::WDFDC>;
|
||||
@@ -133,17 +133,34 @@ void WD1770::run_for(const Cycles cycles) {
|
||||
}
|
||||
}
|
||||
|
||||
#include <iostream>
|
||||
|
||||
void WD1770::posit_event(const int new_event_type) {
|
||||
#define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; interesting_event_mask_ = int(mask); return; case __LINE__:
|
||||
#define WAIT_FOR_TIME(ms) resume_point_ = __LINE__; delay_time_ = ms * 8000; WAIT_FOR_EVENT(Event1770::Timer);
|
||||
#define WAIT_FOR_BYTES(count) distance_into_section_ = 0; \
|
||||
WAIT_FOR_EVENT(Event::Token); \
|
||||
if(get_latest_token().type == Token::Byte) ++distance_into_section_; \
|
||||
if(distance_into_section_ < count) { \
|
||||
return; \
|
||||
}
|
||||
#define BEGIN_SECTION() switch(resume_point_) { default:
|
||||
#define END_SECTION() (void)0; }
|
||||
#define WAIT_FOR_EVENT(mask) { \
|
||||
interesting_event_mask_ = int(mask); \
|
||||
static constexpr int location = __COUNTER__ + 1; \
|
||||
resume_point_ = location; \
|
||||
return; \
|
||||
case location: \
|
||||
(void)0; \
|
||||
}
|
||||
|
||||
#define WAIT_FOR_TIME(ms) \
|
||||
delay_time_ = ms * 8000; \
|
||||
WAIT_FOR_EVENT(Event1770::Timer);
|
||||
|
||||
#define WAIT_FOR_BYTES(count) \
|
||||
distance_into_section_ = 0; \
|
||||
WAIT_FOR_EVENT(Event::Token); \
|
||||
distance_into_section_ += get_latest_token().type == Token::Byte; \
|
||||
if(distance_into_section_ < count) { \
|
||||
RESUME_WAIT(Event::Token); \
|
||||
}
|
||||
|
||||
#define RESUME_WAIT(mask) interesting_event_mask_ = int(mask); return;
|
||||
|
||||
#define BEGIN_SECTION() switch(resume_point_) { default:
|
||||
#define END_SECTION() (void)0; }
|
||||
|
||||
const auto READ_ID = [&] {
|
||||
if(new_event_type == int(Event::Token)) {
|
||||
@@ -184,7 +201,7 @@ void WD1770::posit_event(const int new_event_type) {
|
||||
|
||||
if(new_event_type == int(Event1770::ForceInterrupt)) {
|
||||
interesting_event_mask_ = 0;
|
||||
resume_point_ = 0;
|
||||
resume_point_ = IdleResumePoint;
|
||||
update_status([] (Status &status) {
|
||||
status.type = Status::One;
|
||||
status.data_request = false;
|
||||
@@ -215,7 +232,7 @@ void WD1770::posit_event(const int new_event_type) {
|
||||
BEGIN_SECTION()
|
||||
|
||||
// Wait for a new command, branch to the appropriate handler.
|
||||
case 0:
|
||||
case IdleResumePoint:
|
||||
wait_for_command:
|
||||
Logger::info().append("Idle...");
|
||||
set_data_mode(DataMode::Scanning);
|
||||
@@ -487,12 +504,8 @@ void WD1770::posit_event(const int new_event_type) {
|
||||
WAIT_FOR_EVENT(Event::Token);
|
||||
if(get_latest_token().type != Token::Byte) goto type2_read_byte;
|
||||
data_ = get_latest_token().byte_value;
|
||||
// Logger::info().append("Posting %02x", data_);
|
||||
update_status([] (Status &status) {
|
||||
status.lost_data |= status.data_request;
|
||||
// if(status.lost_data) {
|
||||
// Logger::info().append("Lost data");
|
||||
// }
|
||||
status.data_request = true;
|
||||
});
|
||||
distance_into_section_++;
|
||||
|
||||
@@ -124,9 +124,12 @@ private:
|
||||
};
|
||||
void posit_event(int type);
|
||||
int interesting_event_mask_;
|
||||
int resume_point_ = 0;
|
||||
Cycles::IntType delay_time_ = 0;
|
||||
|
||||
// Current state machine stap pointer.
|
||||
static constexpr int IdleResumePoint = 0;
|
||||
int resume_point_ = IdleResumePoint;
|
||||
|
||||
// ID buffer
|
||||
uint8_t header_[6];
|
||||
|
||||
|
||||
@@ -12,10 +12,9 @@
|
||||
|
||||
using namespace MOS::MOS6560;
|
||||
|
||||
AudioGenerator::AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
AudioGenerator::AudioGenerator(Outputs::Speaker::TaskQueue &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
|
||||
void AudioGenerator::set_volume(const uint8_t volume) {
|
||||
audio_queue_.enqueue([this, volume]() {
|
||||
volume_ = int16_t(volume) * range_multiplier_;
|
||||
@@ -148,7 +147,7 @@ template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Store>(
|
||||
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(
|
||||
std::size_t, Outputs::Speaker::MonoSample *);
|
||||
|
||||
void AudioGenerator::set_sample_volume_range(std::int16_t range) {
|
||||
void AudioGenerator::set_sample_volume_range(const std::int16_t range) {
|
||||
range_multiplier_ = int16_t(range / 64);
|
||||
}
|
||||
|
||||
|
||||
+14
-25
@@ -19,7 +19,7 @@ namespace MOS::MOS6560 {
|
||||
// audio state
|
||||
class AudioGenerator: public Outputs::Speaker::BufferSource<AudioGenerator, false> {
|
||||
public:
|
||||
AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
AudioGenerator(Outputs::Speaker::TaskQueue &audio_queue);
|
||||
|
||||
void set_volume(uint8_t);
|
||||
void set_control(int channel, uint8_t value);
|
||||
@@ -30,7 +30,7 @@ public:
|
||||
void set_sample_volume_range(std::int16_t);
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Outputs::Speaker::TaskQueue &audio_queue_;
|
||||
|
||||
unsigned int counters_[4] = {2, 1, 0, 0}; // create a slight phase offset for the three channels
|
||||
unsigned int shift_registers_[4] = {0, 0, 0, 0};
|
||||
@@ -64,8 +64,7 @@ public:
|
||||
MOS6560(BusHandler &bus_handler) :
|
||||
bus_handler_(bus_handler),
|
||||
crt_(65*4, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8),
|
||||
audio_generator_(audio_queue_),
|
||||
speaker_(audio_generator_)
|
||||
audio_(Cycles(4))
|
||||
{
|
||||
// default to s-video output
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::SVideo);
|
||||
@@ -75,11 +74,11 @@ public:
|
||||
}
|
||||
|
||||
~MOS6560() {
|
||||
audio_queue_.flush();
|
||||
audio_.stop();
|
||||
}
|
||||
|
||||
void set_clock_rate(const double clock_rate) {
|
||||
speaker_.set_input_rate(float(clock_rate / 4.0));
|
||||
audio_.speaker().set_input_rate(float(clock_rate / 4.0));
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *const scan_target) {
|
||||
@@ -95,11 +94,11 @@ public:
|
||||
return crt_.get_display_type();
|
||||
}
|
||||
Outputs::Speaker::Speaker *get_speaker() {
|
||||
return &speaker_;
|
||||
return &audio_.speaker();
|
||||
}
|
||||
|
||||
void set_high_frequency_cutoff(const float cutoff) {
|
||||
speaker_.set_high_frequency_cutoff(cutoff);
|
||||
audio_.speaker().set_high_frequency_cutoff(cutoff);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -161,10 +160,10 @@ public:
|
||||
|
||||
switch(output_mode) {
|
||||
case OutputMode::PAL:
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.07f, 0.9f, 0.9f));
|
||||
crt_.set_fixed_framing(Outputs::Display::Rect(0.1f, 0.07f, 0.9f, 0.9f));
|
||||
break;
|
||||
case OutputMode::NTSC:
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f));
|
||||
crt_.set_fixed_framing(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -180,7 +179,7 @@ public:
|
||||
*/
|
||||
inline void run_for(const Cycles cycles) {
|
||||
// keep track of the amount of time since the speaker was updated; lazy updates are applied
|
||||
cycles_since_speaker_update_ += cycles;
|
||||
audio_ += cycles;
|
||||
|
||||
auto number_of_cycles = cycles.as_integral();
|
||||
while(number_of_cycles--) {
|
||||
@@ -377,8 +376,7 @@ public:
|
||||
Causes the 6560 to flush as much pending CRT and speaker communications as possible.
|
||||
*/
|
||||
inline void flush() {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
audio_.perform();
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -420,14 +418,12 @@ public:
|
||||
case 0xb:
|
||||
case 0xc:
|
||||
case 0xd:
|
||||
update_audio();
|
||||
audio_generator_.set_control(address - 0xa, value);
|
||||
audio_->set_control(address - 0xa, value);
|
||||
break;
|
||||
|
||||
case 0xe:
|
||||
update_audio();
|
||||
registers_.auxiliary_colour = colours_[value >> 4];
|
||||
audio_generator_.set_volume(value & 0xf);
|
||||
audio_->set_volume(value & 0xf);
|
||||
break;
|
||||
|
||||
case 0xf: {
|
||||
@@ -467,14 +463,7 @@ private:
|
||||
BusHandler &bus_handler_;
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
AudioGenerator audio_generator_;
|
||||
Outputs::Speaker::PullLowpass<AudioGenerator> speaker_;
|
||||
|
||||
Cycles cycles_since_speaker_update_;
|
||||
void update_audio() {
|
||||
speaker_.run_for(audio_queue_, Cycles(cycles_since_speaker_update_.divide(Cycles(4))));
|
||||
}
|
||||
Outputs::Speaker::PullLowpassSpeakerQueue<Cycles, AudioGenerator> audio_;
|
||||
|
||||
// register state
|
||||
struct {
|
||||
|
||||
@@ -76,7 +76,7 @@ public:
|
||||
CRTC6845(BusHandlerT &bus_handler) noexcept :
|
||||
bus_handler_(bus_handler), status_(0) {}
|
||||
|
||||
void select_register(uint8_t r) {
|
||||
void select_register(const uint8_t r) {
|
||||
selected_register_ = r;
|
||||
}
|
||||
|
||||
@@ -94,12 +94,18 @@ public:
|
||||
if(selected_register_ == 16 || selected_register_ == 17) status_ &= ~0x40;
|
||||
|
||||
if(personality == Personality::UM6845R && selected_register_ == 31) return dummy_register_;
|
||||
if(selected_register_ < 12 || selected_register_ > 17) return 0xff;
|
||||
return registers_[selected_register_];
|
||||
|
||||
// Registers below 12 are write-only; no registers are defined above position 17
|
||||
// (other than the UM6845R-specific test register as per above).
|
||||
//
|
||||
// Per the BBC Wiki, attempting to read such a register results in 0.
|
||||
if(selected_register_ < 12 || selected_register_ > 17) return 0x00;
|
||||
|
||||
return registers_[selected_register_.get()];
|
||||
}
|
||||
|
||||
void set_register(const uint8_t value) {
|
||||
switch(selected_register_) {
|
||||
switch(selected_register_.get()) {
|
||||
case 0: layout_.horizontal.total = value; break;
|
||||
case 1: layout_.horizontal.displayed = value; break;
|
||||
case 2: layout_.horizontal.start_sync = value; break;
|
||||
@@ -121,13 +127,13 @@ public:
|
||||
}
|
||||
|
||||
// Per CPC documentation, skew doesn't work on a "type 1 or 2", i.e. an MC6845 or a UM6845R.
|
||||
if(personality != Personality::UM6845R && personality != Personality::MC6845) {
|
||||
switch((value >> 4)&3) {
|
||||
default: display_skew_mask_ = 1; break;
|
||||
case 1: display_skew_mask_ = 2; break;
|
||||
case 2: display_skew_mask_ = 4; break;
|
||||
}
|
||||
}
|
||||
// if(personality != Personality::UM6845R && personality != Personality::MC6845) {
|
||||
// switch((value >> 4)&3) {
|
||||
// default: display_skew_mask_ = 1; break;
|
||||
// case 1: display_skew_mask_ = 2; break;
|
||||
// case 2: display_skew_mask_ = 4; break;
|
||||
// }
|
||||
// }
|
||||
break;
|
||||
case 9: layout_.vertical.end_line = value; break;
|
||||
case 10:
|
||||
@@ -156,15 +162,15 @@ public:
|
||||
|
||||
0x7f, // Start horizontal retrace.
|
||||
0x1f, 0x7f, 0x7f,
|
||||
0xff, 0x1f, 0x7f, 0x1f,
|
||||
0xfc, 0x1f, 0x7f, 0x1f,
|
||||
uint8_t(RefreshAddress::Mask >> 8), uint8_t(RefreshAddress::Mask),
|
||||
uint8_t(RefreshAddress::Mask >> 8), uint8_t(RefreshAddress::Mask),
|
||||
};
|
||||
|
||||
if(selected_register_ < 16) {
|
||||
registers_[selected_register_] = value & masks[selected_register_];
|
||||
registers_[selected_register_.get()] = value & masks[selected_register_.get()];
|
||||
}
|
||||
if(selected_register_ == 31 && personality == Personality::UM6845R) {
|
||||
if(selected_register_.get() == 31 && personality == Personality::UM6845R) {
|
||||
dummy_register_ = value;
|
||||
}
|
||||
}
|
||||
@@ -486,9 +492,6 @@ private:
|
||||
/// Provide interlaced sync and scan even/odd lines depending on field.
|
||||
SyncAndVideo,
|
||||
};
|
||||
enum class BlinkMode {
|
||||
// TODO.
|
||||
};
|
||||
|
||||
// Comments on the right provide the corresponding signal name in hoglet's VHDL implementation.
|
||||
struct {
|
||||
@@ -521,7 +524,7 @@ private:
|
||||
|
||||
uint8_t registers_[18]{};
|
||||
uint8_t dummy_register_ = 0;
|
||||
int selected_register_ = 0;
|
||||
Numeric::SizedInt<5> selected_register_ = 0;
|
||||
|
||||
CharacterAddress character_counter_; // h_counter
|
||||
Numeric::SizedInt<3> character_reset_history_; // sol
|
||||
@@ -530,7 +533,6 @@ private:
|
||||
LineAddress line_; // line_counter
|
||||
LineAddress next_line_; // line_counter_next
|
||||
RefreshAddress refresh_; // ma_i
|
||||
uint8_t adjustment_counter_ = 0;
|
||||
|
||||
bool character_is_visible_ = false; // h_display
|
||||
bool row_is_visible_ = false; // v_display
|
||||
@@ -546,8 +548,7 @@ private:
|
||||
RefreshAddress line_address_; // ma_row
|
||||
uint8_t status_ = 0;
|
||||
|
||||
int display_skew_mask_ = 1;
|
||||
unsigned int character_is_visible_shifter_ = 0;
|
||||
// int display_skew_mask_ = 1;
|
||||
|
||||
bool eof_latched_ = false; // eof_latched
|
||||
bool eom_latched_ = false; // eom_latched
|
||||
@@ -558,8 +559,6 @@ private:
|
||||
bool vsync_even_ = false; // vs_even
|
||||
bool vsync_odd_ = false; // vs_odd
|
||||
|
||||
bool reset_ = false;
|
||||
|
||||
Numeric::SizedInt<3> cursor_history_; // cursor0, cursor1, cursor2 [TODO]
|
||||
bool line_is_interlaced_ = false;
|
||||
|
||||
|
||||
+29
-19
@@ -58,15 +58,18 @@ void i8272::run_for(const Cycles cycles) {
|
||||
drives_[c].step_rate_counter %= (8000 * step_rate_time_);
|
||||
while(steps--) {
|
||||
// Perform a step.
|
||||
int direction = (drives_[c].target_head_position < drives_[c].head_position) ? -1 : 1;
|
||||
Logger::info().append(
|
||||
"Target %d versus believed %d", drives_[c].target_head_position, drives_[c].head_position);
|
||||
const int direction = (drives_[c].target_head_position < drives_[c].head_position) ? -1 : 1;
|
||||
select_drive(c);
|
||||
get_drive().step(Storage::Disk::HeadPosition(direction));
|
||||
if(drives_[c].target_head_position >= 0) drives_[c].head_position += direction;
|
||||
|
||||
Logger::info().append(
|
||||
"Drive %d: seeking %d but seemingly at %d", c, drives_[c].target_head_position, drives_[c].head_position);
|
||||
|
||||
// Check for completion.
|
||||
if(seek_is_satisfied(c)) {
|
||||
Logger::info().append(
|
||||
"Drive %d: seek satisfied", c, drives_[c].target_head_position, drives_[c].head_position);
|
||||
drives_[c].phase = Drive::CompletedSeeking;
|
||||
drives_seeking_--;
|
||||
break;
|
||||
@@ -141,22 +144,30 @@ uint8_t i8272::read(const int address) {
|
||||
}
|
||||
|
||||
void i8272::posit_event(const int event_type) {
|
||||
#define BEGIN_SECTION() switch(resume_point_) { default:
|
||||
|
||||
#define BEGIN_SECTION() switch(resume_point_) { default: case IdleResumePoint:
|
||||
#define END_SECTION() }
|
||||
|
||||
#define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; \
|
||||
interesting_event_mask_ = int(mask); \
|
||||
return; \
|
||||
case __LINE__:
|
||||
#define WAIT_FOR_EVENT(mask) { \
|
||||
static constexpr int location = __COUNTER__ + 1; \
|
||||
resume_point_ = location; \
|
||||
interesting_event_mask_ = int(mask); \
|
||||
return; \
|
||||
case location: \
|
||||
(void)0; \
|
||||
}
|
||||
|
||||
#define WAIT_FOR_TIME(ms) interesting_event_mask_ = int(Event8272::Timer); \
|
||||
delay_time_ = ms_to_cycles(ms); \
|
||||
is_sleeping_ = false; \
|
||||
update_clocking_observer(); \
|
||||
resume_point_ = __LINE__; \
|
||||
[[fallthrough]]; \
|
||||
case __LINE__: \
|
||||
if(delay_time_) return;
|
||||
#define WAIT_FOR_TIME(ms) { \
|
||||
static constexpr int location = __COUNTER__ + 1; \
|
||||
interesting_event_mask_ = int(Event8272::Timer); \
|
||||
delay_time_ = ms_to_cycles(ms); \
|
||||
is_sleeping_ = false; \
|
||||
update_clocking_observer(); \
|
||||
resume_point_ = location; \
|
||||
[[fallthrough]]; \
|
||||
case location: \
|
||||
if(delay_time_) return; \
|
||||
}
|
||||
|
||||
#define PASTE(x, y) x##y
|
||||
#define LABEL(x, y) PASTE(x, y)
|
||||
@@ -716,7 +727,6 @@ void i8272::posit_event(const int event_type) {
|
||||
|
||||
// Performs sense interrupt status.
|
||||
sense_interrupt_status:
|
||||
Logger::info().append("Sense interrupt status");
|
||||
{
|
||||
// Find the first drive that is in the CompletedSeeking state.
|
||||
int found_drive = -1;
|
||||
@@ -731,12 +741,12 @@ void i8272::posit_event(const int event_type) {
|
||||
if(found_drive != -1) {
|
||||
drives_[found_drive].phase = Drive::NotSeeking;
|
||||
status_.set_status0(uint8_t(found_drive | uint8_t(Status0::SeekEnded)));
|
||||
// status_.end_sense_interrupt_status(found_drive, 0);
|
||||
// status_.set(Status0::SeekEnded);
|
||||
|
||||
result_stack_ = { drives_[found_drive].head_position, status_[0]};
|
||||
Logger::info().append("Sense interrupt status: returning %02x %02x", result_stack_[0], result_stack_[1]);
|
||||
} else {
|
||||
result_stack_ = { 0x80 };
|
||||
Logger::info().append("Sense interrupt status: returning %02x", result_stack_[0]);
|
||||
}
|
||||
}
|
||||
goto post_result;
|
||||
|
||||
@@ -72,9 +72,11 @@ private:
|
||||
};
|
||||
void posit_event(int type) final;
|
||||
int interesting_event_mask_ = int(Event8272::CommandByte);
|
||||
int resume_point_ = 0;
|
||||
bool is_access_command_ = false;
|
||||
|
||||
static constexpr int IdleResumePoint = 0;
|
||||
int resume_point_ = 0;
|
||||
|
||||
// The counter used for ::Timer events.
|
||||
Cycles::IntType delay_time_ = 0;
|
||||
|
||||
|
||||
@@ -74,9 +74,9 @@ TMS9918<personality>::TMS9918() {
|
||||
this->crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
|
||||
if constexpr (is_yamaha_vdp(personality)) {
|
||||
this->crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.065f, 0.875f, 0.875f));
|
||||
this->crt_.set_fixed_framing(Outputs::Display::Rect(0.07f, 0.065f, 0.875f, 0.875f));
|
||||
} else {
|
||||
this->crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f));
|
||||
this->crt_.set_fixed_framing(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f));
|
||||
}
|
||||
|
||||
// The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement
|
||||
@@ -683,13 +683,7 @@ void Base<personality>::output_border(int cycles, [[maybe_unused]] const uint32_
|
||||
return;
|
||||
}
|
||||
|
||||
// If the border colour is 0, that can be communicated
|
||||
// more efficiently as an explicit blank.
|
||||
if(border_colour) {
|
||||
crt_.output_level<uint32_t>(cycles, border_colour);
|
||||
} else {
|
||||
crt_.output_blank(cycles);
|
||||
}
|
||||
crt_.output_level<uint32_t>(cycles, border_colour);
|
||||
}
|
||||
|
||||
// MARK: - External interface.
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
//
|
||||
// SID.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/11/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "SID.hpp"
|
||||
|
||||
// Sources used:
|
||||
//
|
||||
// (1) SID Article v0.2 at https://github.com/ImreOlajos/SID-Article
|
||||
// (2) Technical SID Information/Software stuff at http://www.sidmusic.org/sid/sidtech2.html
|
||||
// (3) SID 6581/8580 (Sound Interface Device) reference at https://oxyron.de/html/registers_sid.html
|
||||
|
||||
using namespace MOS::SID;
|
||||
|
||||
SID::SID(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
audio_queue_(audio_queue),
|
||||
output_filter_(
|
||||
SignalProcessing::BiquadFilter::Type::LowPass,
|
||||
1000000.0f,
|
||||
15000.0f
|
||||
) {}
|
||||
|
||||
// MARK: - Programmer interface.
|
||||
|
||||
void SID::write(const Numeric::SizedInt<5> address, const uint8_t value) {
|
||||
last_write_ = value;
|
||||
audio_queue_.enqueue([=, this] {
|
||||
const auto voice = [&]() -> Voice & {
|
||||
return voices_[address.get() / 7];
|
||||
};
|
||||
const auto oscillator = [&]() -> Voice::Oscillator & {
|
||||
return voice().oscillator;
|
||||
};
|
||||
const auto adsr = [&]() -> Voice::ADSR & {
|
||||
return voice().adsr;
|
||||
};
|
||||
|
||||
switch(address.get()) {
|
||||
case 0x00: case 0x07: case 0x0e:
|
||||
oscillator().pitch = (oscillator().pitch & 0xff'00'00) | uint32_t(value << 8);
|
||||
break;
|
||||
case 0x01: case 0x08: case 0x0f:
|
||||
oscillator().pitch = (oscillator().pitch & 0x00'ff'00) | uint32_t(value << 16);
|
||||
break;
|
||||
case 0x02: case 0x09: case 0x10:
|
||||
oscillator().pulse_width = (oscillator().pitch & 0xf0'00'00'00) | uint32_t(value << 20);
|
||||
break;
|
||||
case 0x03: case 0x0a: case 0x11:
|
||||
// The top bit of the phase counter is inverted; since it'll be compared directly with the
|
||||
// pulse width, invert that bit too.
|
||||
oscillator().pulse_width =
|
||||
(
|
||||
(oscillator().pitch & 0x0f'f0'00'00) |
|
||||
uint32_t(value << 28)
|
||||
);
|
||||
break;
|
||||
case 0x04: case 0x0b: case 0x12:
|
||||
voice().set_control(value);
|
||||
break;
|
||||
case 0x05: case 0x0c: case 0x13:
|
||||
adsr().attack = value >> 4;
|
||||
adsr().decay = value;
|
||||
adsr().set_phase(adsr().phase);
|
||||
break;
|
||||
case 0x06: case 0x0d: case 0x14:
|
||||
adsr().sustain = (value >> 4) | (value & 0xf0);
|
||||
adsr().release = value;
|
||||
adsr().set_phase(adsr().phase);
|
||||
break;
|
||||
|
||||
case 0x15:
|
||||
filter_cutoff_.load<0, 3>(value);
|
||||
update_filter();
|
||||
break;
|
||||
case 0x16:
|
||||
filter_cutoff_.load<3>(value);
|
||||
update_filter();
|
||||
break;
|
||||
case 0x17:
|
||||
filter_channels_ = value;
|
||||
filter_resonance_ = value >> 4;
|
||||
update_filter();
|
||||
break;
|
||||
case 0x18:
|
||||
volume_ = value & 0x0f;
|
||||
filter_mode_ = value >> 4;
|
||||
voice3_disable_ = value & 0x80;
|
||||
update_filter();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SID::set_potentometer_input(const int index, const uint8_t value) {
|
||||
potentometers_[index] = value;
|
||||
}
|
||||
|
||||
void SID::update_filter() {
|
||||
using Type = SignalProcessing::BiquadFilter::Type;
|
||||
Type type = Type::AllPass;
|
||||
|
||||
switch(filter_mode_.get()) {
|
||||
case 0:
|
||||
filter_ = SignalProcessing::BiquadFilter();
|
||||
return;
|
||||
|
||||
case 1:
|
||||
case 3: type = Type::LowPass; break;
|
||||
|
||||
case 2: type = Type::BandPass; break;
|
||||
case 5: type = Type::Notch; break;
|
||||
|
||||
case 4:
|
||||
case 6: type = Type::HighPass; break;
|
||||
|
||||
case 7: type = Type::AllPass; break;
|
||||
}
|
||||
|
||||
filter_.configure(
|
||||
type,
|
||||
1'000'000.0f,
|
||||
30.0f + float(filter_cutoff_.get()) * 5.8f,
|
||||
0.707f + float(filter_resonance_.get()) * 0.2862f,
|
||||
6.0f,
|
||||
true
|
||||
);
|
||||
|
||||
// Filter cutoff: the data sheet provides that it is linear, and "approximate Cutoff Frequency
|
||||
// ranges between 30Hz and 12KHz [with recommended externally-supplied capacitors]."
|
||||
//
|
||||
// It's an 11-bit number, so the above is "approximate"ly right.
|
||||
|
||||
// Resonance: a complete from-thin-air guess. The data sheet says merely:
|
||||
//
|
||||
// "There are 16 Resonance settings ranging from about 0.707 (Critical Damping) for a count of 0
|
||||
// to a maximum for a count of 15"
|
||||
//
|
||||
// i.e. no information is given on the maximum. I've taken it to be 5-ish per commentary on more general sites
|
||||
// that 5 is a typical ceiling for the resonance factor.
|
||||
}
|
||||
|
||||
uint8_t SID::read(const Numeric::SizedInt<5> address) {
|
||||
switch(address.get()) {
|
||||
default: return last_write_;
|
||||
|
||||
case 0x19: return potentometers_[0];
|
||||
case 0x1a: return potentometers_[1];
|
||||
|
||||
case 0x1b:
|
||||
case 0x1c:
|
||||
// Ensure all channels are entirely up to date.
|
||||
audio_queue_.spin_flush();
|
||||
return (address == 0x1c) ? voices_[2].adsr.envelope : uint8_t(voices_[2].output(voices_[1]) >> 4);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Oscillators.
|
||||
|
||||
void Voice::Oscillator::reset_phase() {
|
||||
phase = PhaseReload;
|
||||
}
|
||||
|
||||
bool Voice::Oscillator::did_raise_b23() const {
|
||||
return previous_phase > phase;
|
||||
}
|
||||
|
||||
bool Voice::Oscillator::did_raise_b19() const {
|
||||
static constexpr int NoiseBit = 1 << (19 + 8);
|
||||
return (previous_phase ^ phase) & phase & NoiseBit;
|
||||
}
|
||||
|
||||
uint16_t Voice::Oscillator::sawtooth_output() const {
|
||||
return (phase >> 20) ^ 0x800;
|
||||
}
|
||||
|
||||
// MARK: - Noise generator.
|
||||
|
||||
uint16_t Voice::NoiseGenerator::output() const {
|
||||
// Uses bits: 20, 18, 14, 11, 9, 5, 2 and 0, plus four more zero bits.
|
||||
const uint16_t output =
|
||||
((noise >> 9) & 0b1000'0000'0000) | // b20 -> b11
|
||||
((noise >> 8) & 0b0100'0000'0000) | // b18 -> b10
|
||||
((noise >> 5) & 0b0010'0000'0000) | // b14 -> b9
|
||||
((noise >> 3) & 0b0001'0000'0000) | // b11 -> b8
|
||||
((noise >> 2) & 0b0000'1000'0000) | // b9 -> b7
|
||||
((noise << 1) & 0b0000'0100'0000) | // b5 -> b6
|
||||
((noise << 3) & 0b0000'0010'0000) | // b2 -> b5
|
||||
((noise << 4) & 0b0000'0001'0000); // b0 -> b4
|
||||
|
||||
assert(output <= Voice::MaxWaveformValue);
|
||||
return output;
|
||||
}
|
||||
|
||||
void Voice::NoiseGenerator::update(const bool test) {
|
||||
noise =
|
||||
(noise << 1) |
|
||||
(((noise >> 17) ^ ((noise >> 22) | test)) & 1);
|
||||
}
|
||||
|
||||
// MARK: - ADSR.
|
||||
|
||||
void Voice::ADSR::set_phase(const Phase new_phase) {
|
||||
static constexpr uint16_t rate_prescaler[] = {
|
||||
9, 32, 63, 95, 149, 220, 267, 313, 392, 977, 1954, 3126, 3907, 11720, 19532, 31251
|
||||
};
|
||||
static_assert(sizeof(rate_prescaler) / sizeof(*rate_prescaler) == 16);
|
||||
|
||||
phase = new_phase;
|
||||
switch(phase) {
|
||||
case Phase::Attack: rate_counter_target = rate_prescaler[attack.get()]; break;
|
||||
case Phase::DecayAndHold: rate_counter_target = rate_prescaler[decay.get()]; break;
|
||||
case Phase::Release: rate_counter_target = rate_prescaler[release.get()]; break;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Voices.
|
||||
|
||||
void Voice::set_control(const uint8_t new_control) {
|
||||
const bool old_gate = gate();
|
||||
control = new_control;
|
||||
if(gate() && !old_gate) {
|
||||
adsr.set_phase(ADSR::Phase::Attack);
|
||||
} else if(!gate() && old_gate) {
|
||||
adsr.set_phase(ADSR::Phase::Release);
|
||||
}
|
||||
}
|
||||
|
||||
bool Voice::noise() const { return control.bit<7>(); }
|
||||
bool Voice::pulse() const { return control.bit<6>(); }
|
||||
bool Voice::sawtooth() const { return control.bit<5>(); }
|
||||
bool Voice::triangle() const { return control.bit<4>(); }
|
||||
bool Voice::test() const { return control.bit<3>(); }
|
||||
bool Voice::ring_mod() const { return control.bit<2>(); }
|
||||
bool Voice::sync() const { return control.bit<1>(); }
|
||||
bool Voice::gate() const { return control.bit<0>(); }
|
||||
|
||||
void Voice::update() {
|
||||
// Oscillator.
|
||||
oscillator.previous_phase = oscillator.phase;
|
||||
if(test()) {
|
||||
oscillator.phase = 0;
|
||||
} else {
|
||||
oscillator.phase += oscillator.pitch;
|
||||
|
||||
if(oscillator.did_raise_b19()) {
|
||||
noise_generator.update(test());
|
||||
}
|
||||
}
|
||||
|
||||
// ADSR.
|
||||
|
||||
// First prescalar, which is a function of the programmer-set rate.
|
||||
++ adsr.rate_counter;
|
||||
if(adsr.rate_counter == adsr.rate_counter_target) {
|
||||
adsr.rate_counter = 0;
|
||||
|
||||
// Second prescalar, which approximates an exponential.
|
||||
static constexpr uint8_t exponential_prescaler[] = {
|
||||
1, // 0
|
||||
30, 30, 30, 30, 30, 30, // 1–6
|
||||
16, 16, 16, 16, 16, 16, 16, 16, // 7–14
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 15–26
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // 27–54
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 55–94
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1,
|
||||
};
|
||||
static_assert(sizeof(exponential_prescaler) == 256);
|
||||
static_assert(exponential_prescaler[0] == 1);
|
||||
static_assert(exponential_prescaler[1] == 30);
|
||||
static_assert(exponential_prescaler[6] == 30);
|
||||
static_assert(exponential_prescaler[7] == 16);
|
||||
static_assert(exponential_prescaler[14] == 16);
|
||||
static_assert(exponential_prescaler[15] == 8);
|
||||
static_assert(exponential_prescaler[26] == 8);
|
||||
static_assert(exponential_prescaler[27] == 4);
|
||||
static_assert(exponential_prescaler[54] == 4);
|
||||
static_assert(exponential_prescaler[55] == 2);
|
||||
static_assert(exponential_prescaler[94] == 2);
|
||||
static_assert(exponential_prescaler[95] == 1);
|
||||
static_assert(exponential_prescaler[255] == 1);
|
||||
|
||||
if(adsr.phase == ADSR::Phase::Attack) {
|
||||
++adsr.envelope;
|
||||
// TODO: what really resets the exponential counter? If anything?
|
||||
adsr.exponential_counter = 0;
|
||||
|
||||
if(adsr.envelope == 0xff) {
|
||||
adsr.set_phase(ADSR::Phase::DecayAndHold);
|
||||
}
|
||||
} else {
|
||||
++adsr.exponential_counter;
|
||||
if(adsr.exponential_counter == exponential_prescaler[adsr.envelope]) {
|
||||
adsr.exponential_counter = 0;
|
||||
|
||||
if(adsr.envelope && (adsr.envelope != adsr.sustain || adsr.phase != ADSR::Phase::DecayAndHold)) {
|
||||
--adsr.envelope;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Voice::synchronise(const Voice &prior) {
|
||||
// Only oscillator work to do here.
|
||||
if(
|
||||
sync() &&
|
||||
prior.oscillator.did_raise_b23()
|
||||
) {
|
||||
oscillator.phase = Oscillator::PhaseReload;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t Voice::pulse_output() const {
|
||||
return (
|
||||
(oscillator.phase ^ 0x8000'0000) < oscillator.pulse_width
|
||||
) ? 0 : MaxWaveformValue;
|
||||
}
|
||||
|
||||
uint16_t Voice::triangle_output(const Voice &prior) const {
|
||||
const uint16_t sawtooth = oscillator.sawtooth_output();
|
||||
const uint16_t xor_mask1 = sawtooth;
|
||||
const uint16_t xor_mask2 = ring_mod() ? prior.sawtooth() : 0;
|
||||
const uint16_t xor_mask = ((xor_mask1 ^ xor_mask2) & 0x800) ? 0xfff : 0x000;
|
||||
return ((sawtooth << 1) ^ xor_mask) & 0xfff;
|
||||
}
|
||||
|
||||
uint16_t Voice::output(const Voice &prior) const {
|
||||
// TODO: true composite waves.
|
||||
//
|
||||
// My current understanding on this: if multiple waveforms are enabled, the pull to zero beats the
|
||||
// pull to one on any line where the two compete. But the twist is that the lines are not necessarily
|
||||
// one per bit since they lead to a common ground. Ummm, I think.
|
||||
//
|
||||
// Anyway, first pass: logical AND. It's not right. It will temporarily do.
|
||||
|
||||
uint16_t output = MaxWaveformValue;
|
||||
|
||||
if(pulse()) output &= pulse_output();
|
||||
if(sawtooth()) output &= oscillator.sawtooth_output();
|
||||
if(triangle()) output &= triangle_output(prior);
|
||||
if(noise()) output &= noise_generator.output();
|
||||
|
||||
return (output * adsr.envelope) / 255;
|
||||
}
|
||||
|
||||
// MARK: - Wave generation
|
||||
|
||||
void SID::set_sample_volume_range(const std::int16_t range) {
|
||||
range_ = range;
|
||||
}
|
||||
|
||||
bool SID::is_zero_level() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
template <Outputs::Speaker::Action action>
|
||||
void SID::apply_samples(const std::size_t number_of_samples, Outputs::Speaker::MonoSample *const target) {
|
||||
for(std::size_t c = 0; c < number_of_samples; c++) {
|
||||
// Advance phase.
|
||||
voices_[0].update();
|
||||
voices_[1].update();
|
||||
voices_[2].update();
|
||||
|
||||
// Apply hard synchronisations.
|
||||
voices_[0].synchronise(voices_[2]);
|
||||
voices_[1].synchronise(voices_[0]);
|
||||
voices_[2].synchronise(voices_[1]);
|
||||
|
||||
// Construct filtered and unfiltered output.
|
||||
const uint16_t outputs[3] = {
|
||||
voices_[0].output(voices_[2]),
|
||||
voices_[1].output(voices_[0]),
|
||||
voices_[2].output(voices_[1]),
|
||||
};
|
||||
|
||||
const uint16_t direct_sample =
|
||||
(filter_channels_.bit<0>() ? 0 : outputs[0]) +
|
||||
(filter_channels_.bit<1>() ? 0 : outputs[1]) +
|
||||
(filter_channels_.bit<2>() || voice3_disable_ ? 0 : outputs[2]);
|
||||
|
||||
const int16_t filtered_sample =
|
||||
filter_.apply(
|
||||
(filter_channels_.bit<0>() ? outputs[0] : 0) +
|
||||
(filter_channels_.bit<1>() ? outputs[1] : 0) +
|
||||
(filter_channels_.bit<2>() ? outputs[2] : 0)
|
||||
);
|
||||
|
||||
// Sum, apply volume and output.
|
||||
const auto sample = output_filter_.apply(int16_t(
|
||||
(
|
||||
volume_ * (
|
||||
direct_sample +
|
||||
filtered_sample
|
||||
- 227 // DC offset.
|
||||
)
|
||||
- 88732
|
||||
) / 3
|
||||
));
|
||||
// Maximum range of above: 15 * (4095 * 3 - 227) = [-3405, 180870]
|
||||
// So subtracting 88732 will move to the centre of the range, and 3 is the smallest
|
||||
// integer that avoids clipping.
|
||||
|
||||
Outputs::Speaker::apply<action>(
|
||||
target[c],
|
||||
Outputs::Speaker::MonoSample((sample * range_) >> 16)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
template void SID::apply_samples<Outputs::Speaker::Action::Mix>(
|
||||
std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void SID::apply_samples<Outputs::Speaker::Action::Store>(
|
||||
std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void SID::apply_samples<Outputs::Speaker::Action::Ignore>(
|
||||
std::size_t, Outputs::Speaker::MonoSample *);
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// SID.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/11/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Numeric/SizedInt.hpp"
|
||||
|
||||
#include "Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
#include "SignalProcessing/BiquadFilter.hpp"
|
||||
|
||||
namespace MOS::SID {
|
||||
|
||||
struct Voice {
|
||||
static constexpr uint16_t MaxWaveformValue = (1 << 12) - 1;
|
||||
|
||||
struct Oscillator {
|
||||
// Programmer inputs.
|
||||
uint32_t pitch = 0;
|
||||
uint32_t pulse_width = 0;
|
||||
|
||||
// State.
|
||||
//
|
||||
// A real SID has a 24-bit phase counter and does various things when the top bit transitions from 0 to 1.
|
||||
// This implementation maintains a 32-bit phase counter in which the low byte is unused and the top bit
|
||||
// is inverted. That saves the cost of any masking and makes the 0 -> 1 transition test actually a 1 -> 0
|
||||
// transition test, which can be phrased simply as after < before. Sadly overflow of signed integers is
|
||||
// still undefined behaviour in C++ at the time of writing.
|
||||
static constexpr uint32_t PhaseReload = 0x8000'0000;
|
||||
uint32_t phase = PhaseReload;
|
||||
uint32_t previous_phase = PhaseReload;
|
||||
|
||||
void reset_phase();
|
||||
bool did_raise_b23() const;
|
||||
bool did_raise_b19() const;
|
||||
uint16_t sawtooth_output() const;
|
||||
} oscillator;
|
||||
struct ADSR {
|
||||
// Programmer inputs.
|
||||
Numeric::SizedInt<4> attack;
|
||||
Numeric::SizedInt<4> decay;
|
||||
Numeric::SizedInt<4> release;
|
||||
|
||||
Numeric::SizedInt<8> sustain;
|
||||
|
||||
// State.
|
||||
enum class Phase {
|
||||
Attack,
|
||||
DecayAndHold,
|
||||
Release,
|
||||
} phase = Phase::Release;
|
||||
Numeric::SizedInt<15> rate_counter;
|
||||
Numeric::SizedInt<15> rate_counter_target;
|
||||
|
||||
uint8_t exponential_counter;
|
||||
uint8_t envelope;
|
||||
|
||||
void set_phase(const Phase);
|
||||
} adsr;
|
||||
struct NoiseGenerator {
|
||||
static constexpr uint32_t NoiseReload = 0x7'ffff;
|
||||
uint32_t noise = NoiseReload;
|
||||
|
||||
uint16_t output() const;
|
||||
void update(const bool test);
|
||||
} noise_generator;
|
||||
|
||||
void set_control(const uint8_t);
|
||||
void update();
|
||||
void synchronise(const Voice &prior);
|
||||
uint16_t output(const Voice &prior) const;
|
||||
|
||||
private:
|
||||
Numeric::SizedInt<8> control;
|
||||
bool noise() const;
|
||||
bool pulse() const;
|
||||
bool sawtooth() const;
|
||||
bool triangle() const;
|
||||
bool test() const;
|
||||
bool ring_mod() const;
|
||||
bool sync() const;
|
||||
bool gate() const;
|
||||
|
||||
uint16_t pulse_output() const;
|
||||
uint16_t triangle_output(const Voice &prior) const;
|
||||
};
|
||||
|
||||
class SID: public Outputs::Speaker::BufferSource<SID, false> {
|
||||
public:
|
||||
SID(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
|
||||
void write(Numeric::SizedInt<5> address, uint8_t value);
|
||||
uint8_t read(Numeric::SizedInt<5> address);
|
||||
void set_potentometer_input(int index, uint8_t value);
|
||||
|
||||
// Outputs::Speaker::BufferSource.
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
bool is_zero_level() const;
|
||||
void set_sample_volume_range(std::int16_t);
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Voice voices_[3];
|
||||
|
||||
uint8_t last_write_;
|
||||
|
||||
int16_t range_ = 0;
|
||||
uint8_t volume_ = 0;
|
||||
|
||||
SignalProcessing::BiquadFilter filter_;
|
||||
Numeric::SizedInt<11> filter_cutoff_;
|
||||
Numeric::SizedInt<4> filter_resonance_;
|
||||
Numeric::SizedInt<4> filter_channels_;
|
||||
Numeric::SizedInt<3> filter_mode_;
|
||||
bool voice3_disable_;
|
||||
void update_filter();
|
||||
|
||||
SignalProcessing::BiquadFilter output_filter_;
|
||||
|
||||
uint8_t potentometers_[2]{};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -28,7 +28,7 @@ template <typename Performer> struct TaskQueueStorage {
|
||||
|
||||
protected:
|
||||
void update() {
|
||||
auto time_now = Time::nanos_now();
|
||||
const auto time_now = Time::nanos_now();
|
||||
performer.perform(time_now - last_fired_);
|
||||
last_fired_ = time_now;
|
||||
}
|
||||
@@ -40,9 +40,12 @@ private:
|
||||
/// An implementation detail; provides a no-op implementation of time advances for TaskQueues without a Performer.
|
||||
template <> struct TaskQueueStorage<void> {
|
||||
TaskQueueStorage() {}
|
||||
protected:
|
||||
void update() {}
|
||||
};
|
||||
|
||||
protected:
|
||||
void update() {}
|
||||
struct EnqueueDelegate {
|
||||
virtual std::function<void(void)> prepare_enqueue() = 0;
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -64,12 +67,22 @@ template <> struct TaskQueueStorage<void> {
|
||||
action occupies the asynchronous thread for long enough. So it is not true that @c perform will be
|
||||
called once per action.
|
||||
*/
|
||||
template <bool perform_automatically, bool start_immediately = true, typename Performer = void> class AsyncTaskQueue: public TaskQueueStorage<Performer> {
|
||||
template <
|
||||
bool perform_automatically,
|
||||
bool start_immediately = true,
|
||||
bool use_enqueue_delegate = false,
|
||||
typename Performer = void
|
||||
>
|
||||
class AsyncTaskQueue: public TaskQueueStorage<Performer> {
|
||||
public:
|
||||
void set_enqueue_delegate(EnqueueDelegate *const delegate) {
|
||||
enqueue_delegate_ = delegate;
|
||||
}
|
||||
|
||||
template <typename... Args> AsyncTaskQueue(Args&&... args) :
|
||||
TaskQueueStorage<Performer>(std::forward<Args>(args)...) {
|
||||
if constexpr (start_immediately) {
|
||||
start();
|
||||
start_impl();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +97,10 @@ public:
|
||||
/// on the same thread as the performer, after the performer has been updated
|
||||
/// to 'now'.
|
||||
void enqueue(const std::function<void(void)> &post_action) {
|
||||
std::lock_guard guard(condition_mutex_);
|
||||
const std::lock_guard guard(condition_mutex_);
|
||||
if constexpr (use_enqueue_delegate) {
|
||||
actions_.push_back(enqueue_delegate_->prepare_enqueue());
|
||||
}
|
||||
actions_.push_back(post_action);
|
||||
|
||||
if constexpr (perform_automatically) {
|
||||
@@ -92,8 +108,15 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
/// @returns The number of items currently enqueued.
|
||||
size_t size() {
|
||||
const std::lock_guard guard(condition_mutex_);
|
||||
return actions_.size();
|
||||
}
|
||||
|
||||
/// Causes any enqueued actions that are not yet scheduled to be scheduled.
|
||||
void perform() {
|
||||
static_assert(!perform_automatically);
|
||||
if(actions_.empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -106,7 +129,7 @@ public:
|
||||
/// The queue cannot be restarted; this is a destructive action.
|
||||
void stop() {
|
||||
if(thread_.joinable()) {
|
||||
should_quit_ = true;
|
||||
should_quit_.store(true, std::memory_order_relaxed);
|
||||
enqueue([] {});
|
||||
if constexpr (!perform_automatically) {
|
||||
perform();
|
||||
@@ -119,36 +142,13 @@ public:
|
||||
///
|
||||
/// This is not guaranteed safely to restart a stopped queue.
|
||||
void start() {
|
||||
thread_ = std::thread{
|
||||
[this] {
|
||||
ActionVector actions;
|
||||
|
||||
// Continue until told to quit.
|
||||
while(!should_quit_) {
|
||||
// Wait for new actions to be signalled, and grab them.
|
||||
std::unique_lock lock(condition_mutex_);
|
||||
while(actions_.empty() && !should_quit_) {
|
||||
condition_.wait(lock);
|
||||
}
|
||||
std::swap(actions, actions_);
|
||||
lock.unlock();
|
||||
|
||||
// Update to now (which is possibly a no-op).
|
||||
TaskQueueStorage<Performer>::update();
|
||||
|
||||
// Perform the actions and destroy them.
|
||||
for(const auto &action: actions) {
|
||||
action();
|
||||
}
|
||||
actions.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
static_assert(!start_immediately);
|
||||
start_impl();
|
||||
}
|
||||
|
||||
/// Schedules any remaining unscheduled work, then blocks synchronously
|
||||
/// until all scheduled work has been performed.
|
||||
void flush() {
|
||||
void lock_flush() {
|
||||
std::mutex flush_mutex;
|
||||
std::condition_variable flush_condition;
|
||||
bool has_run = false;
|
||||
@@ -167,11 +167,58 @@ public:
|
||||
flush_condition.wait(lock, [&has_run] { return has_run; });
|
||||
}
|
||||
|
||||
/// Schedules any remaining unscheduled work, then spins
|
||||
/// until all scheduled work has been performed, placing a memory barrier
|
||||
/// in between.
|
||||
void spin_flush() {
|
||||
std::atomic<bool> has_run = false;
|
||||
|
||||
enqueue([&has_run] () {
|
||||
has_run.store(true, std::memory_order::release);
|
||||
});
|
||||
|
||||
if constexpr (!perform_automatically) {
|
||||
perform();
|
||||
}
|
||||
|
||||
while(!has_run.load(std::memory_order::acquire));
|
||||
}
|
||||
|
||||
~AsyncTaskQueue() {
|
||||
stop();
|
||||
}
|
||||
|
||||
private:
|
||||
void start_impl() {
|
||||
thread_ = std::thread{
|
||||
[this] {
|
||||
ActionVector actions;
|
||||
|
||||
// Continue until told to quit.
|
||||
while(!should_quit_.load(std::memory_order_relaxed)) {
|
||||
// Wait for new actions to be signalled, and grab them.
|
||||
std::unique_lock lock(condition_mutex_);
|
||||
condition_.wait(lock, [&] {
|
||||
return !actions_.empty() || should_quit_.load(std::memory_order_relaxed);
|
||||
});
|
||||
std::swap(actions, actions_);
|
||||
lock.unlock();
|
||||
|
||||
// Update to now (which is possibly a no-op).
|
||||
TaskQueueStorage<Performer>::update();
|
||||
|
||||
// Perform the actions and destroy them.
|
||||
for(const auto &action: actions) {
|
||||
action();
|
||||
}
|
||||
actions.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
EnqueueDelegate *enqueue_delegate_ = nullptr;
|
||||
|
||||
// The list of actions waiting be performed. These will be elided,
|
||||
// increasing their latency, if the emulation thread falls behind.
|
||||
using ActionVector = std::vector<std::function<void(void)>>;
|
||||
|
||||
@@ -25,10 +25,12 @@ ReflectableEnum(Display,
|
||||
// ensure unified property naming.
|
||||
//===
|
||||
|
||||
template <typename Owner> class DisplayOption {
|
||||
namespace Options {
|
||||
|
||||
template <typename Owner> class Display {
|
||||
public:
|
||||
Configurable::Display output;
|
||||
DisplayOption(Configurable::Display output) : output(output) {}
|
||||
Display(const Configurable::Display output) noexcept : output(output) {}
|
||||
|
||||
protected:
|
||||
void declare_display_option() {
|
||||
@@ -37,26 +39,38 @@ protected:
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Owner> class QuickloadOption {
|
||||
template <typename Owner> class QuickLoad {
|
||||
public:
|
||||
bool quickload;
|
||||
QuickloadOption(bool quickload) : quickload(quickload) {}
|
||||
bool quick_load;
|
||||
QuickLoad(const bool quick_load) noexcept : quick_load(quick_load) {}
|
||||
|
||||
protected:
|
||||
void declare_quickload_option() {
|
||||
static_cast<Owner *>(this)->declare(&quickload, "quickload");
|
||||
static_cast<Owner *>(this)->declare(&quick_load, "quickload");
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Owner> class QuickbootOption {
|
||||
template <typename Owner> class QuickBoot {
|
||||
public:
|
||||
bool quickboot;
|
||||
QuickbootOption(bool quickboot) : quickboot(quickboot) {}
|
||||
bool quick_boot;
|
||||
QuickBoot(const bool quick_boot) noexcept : quick_boot(quick_boot) {}
|
||||
|
||||
protected:
|
||||
void declare_quickboot_option() {
|
||||
static_cast<Owner *>(this)->declare(&quickboot, "quickboot");
|
||||
static_cast<Owner *>(this)->declare(&quick_boot, "quickboot");
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Owner> class DynamicCrop {
|
||||
public:
|
||||
bool dynamic_crop;
|
||||
DynamicCrop(const bool dynamic_crop) noexcept : dynamic_crop(dynamic_crop) {}
|
||||
|
||||
protected:
|
||||
void declare_dynamic_crop_option() {
|
||||
static_cast<Owner *>(this)->declare(&dynamic_crop, "dynamiccrop");
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+9
-8
@@ -29,7 +29,7 @@ public:
|
||||
/// Defines the broad type of the input.
|
||||
enum Type {
|
||||
// Half-axis inputs.
|
||||
Up, Down, Left, Right,
|
||||
Down, Up, Left, Right,
|
||||
// Full-axis inputs.
|
||||
Horizontal, Vertical,
|
||||
// Fire buttons.
|
||||
@@ -192,8 +192,8 @@ public:
|
||||
const auto analogue_value = [&](const int mask) {
|
||||
switch(mask) {
|
||||
default: return 0.5f;
|
||||
case 0b01: return digital_maximum();
|
||||
case 0b10: return digital_minimum();
|
||||
case 0b01: return digital_minimum();
|
||||
case 0b10: return digital_maximum();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -264,10 +264,11 @@ private:
|
||||
const int mask = [&] {
|
||||
switch(input.type) {
|
||||
default: return 0;
|
||||
case Input::Type::Up: return 1 << 1;
|
||||
case Input::Type::Down: return 1 << 2;
|
||||
case Input::Type::Up: return 1 << 0;
|
||||
case Input::Type::Down: return 1 << 1;
|
||||
|
||||
case Input::Type::Left: return 1 << 2;
|
||||
case Input::Type::Right: return 1 << 3;
|
||||
case Input::Type::Left: return 1 << 4;
|
||||
}
|
||||
} ();
|
||||
if(is_active) {
|
||||
@@ -279,8 +280,8 @@ private:
|
||||
int digital_mask(const Input::Type axis) const {
|
||||
switch(axis) {
|
||||
default: return 0;
|
||||
case Input::Type::Horizontal: return (digital_inputs_ >> 3) & 3;
|
||||
case Input::Type::Vertical: return (digital_inputs_ >> 1) & 3;
|
||||
case Input::Type::Horizontal: return (digital_inputs_ >> 2) & 3;
|
||||
case Input::Type::Vertical: return (digital_inputs_ >> 0) & 3;
|
||||
}
|
||||
}
|
||||
int digital_inputs_ = 0;
|
||||
|
||||
@@ -909,7 +909,7 @@ std::pair<int, typename Decoder<model>::InstructionT> Decoder<model>::decode(
|
||||
destination_ = source_ = memreg;
|
||||
|
||||
switch(reg) {
|
||||
default: return undefined();
|
||||
default: return undefined();
|
||||
|
||||
case 0: set(Operation::SLDT); break;
|
||||
case 1: set(Operation::STR); break;
|
||||
@@ -924,7 +924,7 @@ std::pair<int, typename Decoder<model>::InstructionT> Decoder<model>::decode(
|
||||
destination_ = source_ = memreg;
|
||||
|
||||
switch(reg) {
|
||||
default: return undefined();
|
||||
default: return undefined();
|
||||
|
||||
case 0: set(Operation::SGDT); break;
|
||||
case 1: set(Operation::SIDT); break;
|
||||
|
||||
@@ -204,7 +204,7 @@ struct SegmentDescriptor {
|
||||
/// Accesses must be `>= bounds().begin` and `<= bounds().end`.
|
||||
DescriptorBounds bounds() const { return bounds_; }
|
||||
|
||||
bool present() const { return type_ & 0x80; }
|
||||
bool present() const { return type_ & 0x80; }
|
||||
int privilege_level() const { return (type_ >> 5) & 3; }
|
||||
uint8_t access_rights() const { return uint8_t(type_); }
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ void idiv(
|
||||
FI;
|
||||
ELSE (* quadword/doubleword operation *)
|
||||
temp ← EDX:EAX / SRC; (* signed division *)
|
||||
IF (temp > 7FFFFFFFH) OR (temp < 80000000H) (* if a positive result is greater than 7FFFFFFFH
|
||||
IF (temp > 7FFFFFFFH) OR (temp < 80000000H) (* if a positive result is greater than 7FFFFFFFH
|
||||
or a negative result is less than 80000000H *)
|
||||
THEN #DE; (* divide error *) ;
|
||||
ELSE
|
||||
|
||||
@@ -450,8 +450,12 @@ template <
|
||||
}
|
||||
return;
|
||||
|
||||
case Operation::OUT: Primitive::out<IntT>(port(instruction.destination().source()), pair_low(), context); return;
|
||||
case Operation::IN: Primitive::in<IntT>(port(instruction.source().source()), pair_low(), context); return;
|
||||
case Operation::OUT:
|
||||
Primitive::out<IntT>(port(instruction.destination().source()), pair_low(), context);
|
||||
return;
|
||||
case Operation::IN:
|
||||
Primitive::in<IntT>(port(instruction.source().source()), pair_low(), context);
|
||||
return;
|
||||
|
||||
case Operation::XLAT: Primitive::xlat<AddressT>(instruction, context); return;
|
||||
|
||||
@@ -746,7 +750,7 @@ requires is_context<ContextT>
|
||||
void perform(
|
||||
const Instruction<type> &instruction,
|
||||
ContextT &context,
|
||||
uint32_t source_ip
|
||||
const ip_size_t<ContextT> source_ip
|
||||
) {
|
||||
if constexpr (uses_8086_exceptions(ContextT::model)) {
|
||||
InstructionSet::x86::perform(
|
||||
@@ -843,7 +847,7 @@ requires is_context<ContextT>
|
||||
void fault(
|
||||
const Exception exception,
|
||||
ContextT &context,
|
||||
const uint32_t source_ip
|
||||
const ip_size_t<ContextT> source_ip
|
||||
) {
|
||||
if constexpr (uses_8086_exceptions(ContextT::model)) {
|
||||
InstructionSet::x86::interrupt(
|
||||
|
||||
@@ -299,10 +299,10 @@ void sal(
|
||||
} else {
|
||||
const auto mask = (Numeric::top_bit<IntT>() >> (count - 1));
|
||||
context.flags.template set_from<Flag::Carry>(
|
||||
destination & mask
|
||||
destination & mask
|
||||
);
|
||||
context.flags.template set_from<Flag::Overflow>(IntT(
|
||||
(destination ^ (destination << 1)) & mask
|
||||
(destination ^ (destination << 1)) & mask
|
||||
));
|
||||
destination <<= count;
|
||||
}
|
||||
|
||||
@@ -250,6 +250,10 @@ concept is_context =
|
||||
is_context_real<ContextT> &&
|
||||
(!has_protected_mode<ContextT::model> || is_context_protected<ContextT>);
|
||||
|
||||
/// Gives the instruction pointer size for a given context.
|
||||
template <typename ContextT>
|
||||
using ip_size_t = std::conditional_t<has_32bit_instructions<ContextT::model>, uint32_t, uint16_t>;
|
||||
|
||||
/// Performs @c instruction querying @c registers and/or @c memory as required, using @c io for port input/output,
|
||||
/// and providing any flow control effects to @c flow_controller.
|
||||
///
|
||||
@@ -259,7 +263,7 @@ template <
|
||||
typename ContextT
|
||||
>
|
||||
requires is_context<ContextT>
|
||||
void perform(const Instruction<type> &, ContextT &, uint32_t source_ip);
|
||||
void perform(const Instruction<type> &, ContextT &, ip_size_t<ContextT> source_ip);
|
||||
|
||||
/// Performs an Exception, which includes those generated by external sources.
|
||||
/// @c source_ip is unused if the exception is an instance of `Exception::interrupt` but is required for other internal faults.
|
||||
@@ -267,7 +271,7 @@ template <
|
||||
typename ContextT
|
||||
>
|
||||
requires is_context<ContextT>
|
||||
void fault(Exception, ContextT &, const uint32_t source_ip = 0);
|
||||
void fault(Exception, ContextT &, const ip_size_t<ContextT> source_ip = 0);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -127,8 +127,8 @@ public:
|
||||
template <DescriptorTable table>
|
||||
void set(const DescriptorTablePointer location) {
|
||||
switch(table) {
|
||||
case DescriptorTable::Local: local_ = location; break;
|
||||
case DescriptorTable::Global: global_ = location; break;
|
||||
case DescriptorTable::Local: local_ = location; break;
|
||||
case DescriptorTable::Global: global_ = location; break;
|
||||
case DescriptorTable::Interrupt: interrupt_ = location; break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,13 +396,19 @@ private:
|
||||
//
|
||||
// Mouse scripting; tick at a minimum of frame length.
|
||||
//
|
||||
static constexpr int TickFrequency = 24'000'000 / 50;
|
||||
static constexpr int TickFrequency = ClockRate / 50; // i.e. 480,000
|
||||
Cycles subtractor = cursor_action_subcycle_;
|
||||
cursor_action_subcycle_ += cycles;
|
||||
auto segments = cursor_action_subcycle_.divide(Cycles(TickFrequency)).as<int>();
|
||||
while(segments--) {
|
||||
Cycles next = Cycles(TickFrequency);
|
||||
//
|
||||
// Run up until end of next window.
|
||||
//
|
||||
Cycles next = Cycles(TickFrequency) - subtractor;
|
||||
subtractor = Cycles(0);
|
||||
if(next > cycles) next = cycles;
|
||||
cycles -= next;
|
||||
run(next);
|
||||
|
||||
if(!cursor_actions_.empty()) {
|
||||
const auto move_to_next = [&]() {
|
||||
@@ -413,7 +419,7 @@ private:
|
||||
const auto &action = cursor_actions_.front();
|
||||
switch(action.type) {
|
||||
case CursorAction::Type::MoveTo: {
|
||||
// A measure of where within the tip lies within
|
||||
// A measure of where the tip lies within
|
||||
// the default RISC OS cursor.
|
||||
static constexpr int ActionPointOffset = 20;
|
||||
static constexpr int MaxStep = 24;
|
||||
@@ -457,12 +463,12 @@ private:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Execution proper.
|
||||
//
|
||||
run(next);
|
||||
}
|
||||
|
||||
//
|
||||
// Discharge residue.
|
||||
//
|
||||
run(cycles);
|
||||
}
|
||||
|
||||
template <bool original_speed>
|
||||
@@ -509,13 +515,13 @@ private:
|
||||
// MARK: - Configuration options.
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->quickload = accelerate_loading_;
|
||||
options->quick_load = accelerate_loading_;
|
||||
return options;
|
||||
}
|
||||
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
accelerate_loading_ = options->quickload;
|
||||
accelerate_loading_ = options->quick_load;
|
||||
}
|
||||
|
||||
// MARK: - AudioProducer
|
||||
|
||||
@@ -24,13 +24,13 @@ struct Machine {
|
||||
const ROMMachine::ROMFetcher &rom_fetcher
|
||||
);
|
||||
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::QuickloadOption<Options> {
|
||||
friend Configurable::QuickloadOption<Options>;
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::Options::QuickLoad<Options> {
|
||||
friend Configurable::Options::QuickLoad<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::QuickloadOption<Options>(type == Configurable::OptionsType::UserFriendly) {}
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::Options::QuickLoad<Options>(type == Configurable::OptionsType::UserFriendly) {}
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
declare_quickload_option();
|
||||
|
||||
@@ -27,8 +27,17 @@ struct Video {
|
||||
sound_(sound),
|
||||
ram_(ram),
|
||||
crt_(Outputs::Display::InputDataType::Red4Green4Blue4) {
|
||||
const auto cycles_per_line = static_cast<int>(24'000'000 / (312 * 50));
|
||||
crt_.set_new_timing(
|
||||
cycles_per_line,
|
||||
312, /* Height of display. */
|
||||
Outputs::CRT::PAL::ColourSpace,
|
||||
Outputs::CRT::PAL::ColourCycleNumerator,
|
||||
Outputs::CRT::PAL::ColourCycleDenominator,
|
||||
Outputs::CRT::PAL::VerticalSyncLength,
|
||||
Outputs::CRT::PAL::AlternatesPhase);
|
||||
set_clock_divider(3);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.041f, 0.04f, 0.95f, 0.95f));
|
||||
crt_.set_fixed_framing(Outputs::Display::Rect(0.041f, 0.04f, 0.95f, 0.95f));
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
}
|
||||
|
||||
@@ -210,7 +219,7 @@ struct Video {
|
||||
case Phase::StartInterlacedSync: tick_horizontal<Phase::StartInterlacedSync>(); break;
|
||||
case Phase::EndInterlacedSync: tick_horizontal<Phase::EndInterlacedSync>(); break;
|
||||
}
|
||||
++time_in_phase_;
|
||||
time_in_phase_ += clock_divider_;
|
||||
}
|
||||
|
||||
/// @returns @c true if a vertical retrace interrupt has been signalled since the last call to @c interrupt(); @c false otherwise.
|
||||
@@ -476,15 +485,6 @@ private:
|
||||
}
|
||||
|
||||
clock_divider_ = divider;
|
||||
const auto cycles_per_line = static_cast<int>(24'000'000 / (divider * 312 * 50));
|
||||
crt_.set_new_timing(
|
||||
cycles_per_line,
|
||||
312, /* Height of display. */
|
||||
Outputs::CRT::PAL::ColourSpace,
|
||||
Outputs::CRT::PAL::ColourCycleNumerator,
|
||||
Outputs::CRT::PAL::ColourCycleDenominator,
|
||||
Outputs::CRT::PAL::VerticalSyncLength,
|
||||
Outputs::CRT::PAL::AlternatesPhase);
|
||||
clock_rate_observer_.update_clock_rates();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//
|
||||
//
|
||||
// BBCMicro.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
@@ -14,11 +15,16 @@
|
||||
#include "Machines/Utility/MemoryFuzzer.hpp"
|
||||
#include "Machines/Utility/Typer.hpp"
|
||||
|
||||
#include "Processors/6502/6502.hpp"
|
||||
#include "Processors/6502Mk2/6502Mk2.hpp"
|
||||
|
||||
#include "Machines/Acorn/Tube/ULA.hpp"
|
||||
#include "Machines/Acorn/Tube/Tube6502.hpp"
|
||||
#include "Machines/Acorn/Tube/TubeZ80.hpp"
|
||||
|
||||
#include "Components/6522/6522.hpp"
|
||||
#include "Components/6845/CRTC6845.hpp"
|
||||
#include "Components/6850/6850.hpp"
|
||||
#include "Components/SID/SID.hpp"
|
||||
#include "Components/SAA5050/SAA5050.hpp"
|
||||
#include "Components/SN76489/SN76489.hpp"
|
||||
#include "Components/uPD7002/uPD7002.hpp"
|
||||
@@ -30,6 +36,7 @@
|
||||
#include "Outputs/Log.hpp"
|
||||
|
||||
#include "Outputs/CRT/CRT.hpp"
|
||||
#include "Outputs/Speaker/Implementation/CompoundSource.hpp"
|
||||
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "Concurrency/AsyncTaskQueue.hpp"
|
||||
|
||||
@@ -45,6 +52,9 @@ namespace BBCMicro {
|
||||
|
||||
namespace {
|
||||
using Logger = Log::Logger<Log::Source::BBCMicro>;
|
||||
using TubeProcessor = Analyser::Static::Acorn::BBCMicroTarget::TubeProcessor;
|
||||
|
||||
// MARK: - Joysticks.
|
||||
|
||||
/*!
|
||||
Provides an analogue joystick with a single fire button.
|
||||
@@ -93,25 +103,54 @@ private:
|
||||
const int first_channel_;
|
||||
bool fire_ = false;
|
||||
};
|
||||
|
||||
// MARK: - Lazy audio holder.
|
||||
|
||||
/*!
|
||||
Combines an SN76489 with an appropriate asynchronous queue and filtering speaker.
|
||||
*/
|
||||
|
||||
// TODO: generalise the below and clean up across the project.
|
||||
template <bool has_beebsid>
|
||||
struct Audio {
|
||||
private:
|
||||
using CompoundSource = Outputs::Speaker::CompoundSource<TI::SN76489, MOS::SID::SID>;
|
||||
using Source = std::conditional_t<has_beebsid, CompoundSource, TI::SN76489>;
|
||||
using Speaker = Outputs::Speaker::PullLowpass<Source>;
|
||||
|
||||
Source &speaker_source() {
|
||||
if constexpr (has_beebsid) {
|
||||
return compound_;
|
||||
} else {
|
||||
return sn76489_;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
Audio() :
|
||||
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, 2),
|
||||
speaker_(sn76489_)
|
||||
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, 4),
|
||||
sid_(audio_queue_),
|
||||
compound_(sn76489_, sid_),
|
||||
speaker_(speaker_source())
|
||||
{
|
||||
// Combined with the additional divider specified above, implies this chip is clocked at 4Mhz.
|
||||
speaker_.set_input_rate(2'000'000.0f);
|
||||
// Combined with the additional divider specified above, implies the SN76489 is clocked at 4Mhz.
|
||||
speaker_.set_input_rate(1'000'000.0f);
|
||||
}
|
||||
|
||||
~Audio() {
|
||||
audio_queue_.flush();
|
||||
audio_queue_.lock_flush();
|
||||
}
|
||||
|
||||
TI::SN76489 *operator ->() {
|
||||
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
|
||||
return &sn76489_;
|
||||
template <typename TargetT>
|
||||
TargetT &get() {
|
||||
post_time();
|
||||
|
||||
if constexpr (std::is_same_v<TargetT, TI::SN76489>) {
|
||||
return sn76489_;
|
||||
}
|
||||
if constexpr (std::is_same_v<TargetT, MOS::SID::SID>) {
|
||||
return sid_;
|
||||
}
|
||||
}
|
||||
|
||||
void operator +=(const Cycles duration) {
|
||||
@@ -119,7 +158,7 @@ struct Audio {
|
||||
}
|
||||
|
||||
void flush() {
|
||||
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
|
||||
post_time();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
@@ -127,10 +166,20 @@ struct Audio {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
size_t queue_size() {
|
||||
return audio_queue_.size();
|
||||
}
|
||||
|
||||
private:
|
||||
void post_time() {
|
||||
speaker_.run_for(audio_queue_, time_since_update_.divide(Cycles(2)));
|
||||
}
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
Outputs::Speaker::PullLowpass<TI::SN76489> speaker_;
|
||||
MOS::SID::SID sid_;
|
||||
CompoundSource compound_;
|
||||
Outputs::Speaker::PullLowpass<Source> speaker_;
|
||||
Cycles time_since_update_;
|
||||
};
|
||||
|
||||
@@ -158,23 +207,31 @@ protected:
|
||||
uint16_t video_base_ = 0;
|
||||
};
|
||||
|
||||
// MARK: - VIAs.
|
||||
|
||||
/*!
|
||||
Models the system VIA, which connects to the SN76489 and the keyboard.
|
||||
*/
|
||||
struct SystemVIAPortHandler;
|
||||
using SystemVIA = MOS::MOS6522::MOS6522<SystemVIAPortHandler>;
|
||||
struct VSyncReceiver {
|
||||
virtual void set_vsync(bool) = 0;
|
||||
};
|
||||
struct SystemVIADelegate {
|
||||
virtual void strobe_lightpen() = 0;
|
||||
};
|
||||
|
||||
struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
template <typename AudioT>
|
||||
struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler, public VSyncReceiver {
|
||||
SystemVIAPortHandler(
|
||||
Audio &audio,
|
||||
AudioT &audio,
|
||||
VideoBaseAddress &video_base,
|
||||
SystemVIA &via,
|
||||
MOS::MOS6522::MOS6522<SystemVIAPortHandler<AudioT>> &via,
|
||||
SystemVIADelegate &delegate,
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks,
|
||||
const bool run_disk
|
||||
) :
|
||||
audio_(audio), video_base_(video_base), via_(via), joysticks_(joysticks)
|
||||
audio_(audio), video_base_(video_base), via_(via), joysticks_(joysticks), delegate_(delegate)
|
||||
{
|
||||
set_key_flag(6, run_disk);
|
||||
set_key_flag(uint8_t(Key::Bit3), run_disk);
|
||||
}
|
||||
|
||||
// CA2: key pressed;
|
||||
@@ -203,7 +260,7 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
|
||||
// Check for a strobe on the audio output.
|
||||
if((old_latch^latch_) & old_latch & LatchFlags::WriteToSN76489) {
|
||||
audio_->write(port_a_output_);
|
||||
audio_.template get<TI::SN76489>().write(port_a_output_);
|
||||
}
|
||||
|
||||
// Pass on the video wraparound/base.
|
||||
@@ -221,12 +278,16 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
|
||||
if(new_caps != caps_led_state_) {
|
||||
caps_led_state_ = new_caps;
|
||||
activity_observer_->set_led_status(caps_led, caps_led_state_);
|
||||
if(activity_observer_) {
|
||||
activity_observer_->set_led_status(caps_led, caps_led_state_);
|
||||
}
|
||||
}
|
||||
|
||||
if(new_shift != shift_led_state_) {
|
||||
shift_led_state_ = new_shift;
|
||||
activity_observer_->set_led_status(shift_led, shift_led_state_);
|
||||
if(activity_observer_) {
|
||||
activity_observer_->set_led_status(shift_led, shift_led_state_);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,6 +314,16 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
return key_state;
|
||||
}
|
||||
|
||||
template<MOS::MOS6522::Port port, MOS::MOS6522::Line line>
|
||||
void set_control_line_output(const bool value) {
|
||||
if constexpr (port == MOS::MOS6522::Port::B && line == MOS::MOS6522::Line::Two) {
|
||||
if(previous_cb2_ != value && !value) {
|
||||
delegate_.strobe_lightpen();
|
||||
}
|
||||
previous_cb2_ = value;
|
||||
}
|
||||
}
|
||||
|
||||
void set_key(const uint8_t key, const bool pressed) {
|
||||
set_key_flag(key, pressed);
|
||||
update_ca2();
|
||||
@@ -289,6 +360,10 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
}
|
||||
}
|
||||
|
||||
bool caps_lock() const {
|
||||
return caps_led_state_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t latch_ = 0;
|
||||
enum LatchFlags: uint8_t {
|
||||
@@ -297,11 +372,12 @@ private:
|
||||
};
|
||||
|
||||
uint8_t port_a_output_ = 0;
|
||||
bool previous_cb2_ = false;
|
||||
|
||||
Audio &audio_;
|
||||
AudioT &audio_;
|
||||
VideoBaseAddress &video_base_;
|
||||
|
||||
SystemVIA &via_;
|
||||
MOS::MOS6522::MOS6522<SystemVIAPortHandler<AudioT>> &via_;
|
||||
|
||||
// MARK: - Keyboard state and helpers.
|
||||
|
||||
@@ -332,7 +408,7 @@ private:
|
||||
}
|
||||
} ()).to_ulong() & 0xfe; // Discard the first row.
|
||||
|
||||
via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two>(state);
|
||||
via_.template set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two>(state);
|
||||
}
|
||||
|
||||
static inline const std::string caps_led = "CAPS";
|
||||
@@ -342,19 +418,39 @@ private:
|
||||
Activity::Observer *activity_observer_ = nullptr;
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks_;
|
||||
SystemVIADelegate &delegate_;
|
||||
|
||||
void set_vsync(const bool vsync) override {
|
||||
via_.template set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(vsync);
|
||||
}
|
||||
};
|
||||
|
||||
// MARK: - CRTC output.
|
||||
|
||||
/*!
|
||||
Handles CRTC bus activity.
|
||||
*/
|
||||
class CRTCBusHandler: public VideoBaseAddress {
|
||||
public:
|
||||
CRTCBusHandler(const uint8_t *const ram, SystemVIA &system_via) :
|
||||
CRTCBusHandler(const uint8_t *const ram, VSyncReceiver &vsync_receiver) :
|
||||
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1),
|
||||
ram_(ram),
|
||||
system_via_(system_via)
|
||||
{
|
||||
crt_.set_visible_area(crt_.get_rect_for_area(30, 256, 160, 800, 4.0f / 3.0f));
|
||||
vsync_receiver_(vsync_receiver)
|
||||
{}
|
||||
|
||||
void set_dynamic_framing(const bool enable) {
|
||||
dynamic_framing_ = enable;
|
||||
if(enable) {
|
||||
crt_.set_dynamic_framing(
|
||||
Outputs::Display::Rect(0.13333f, 0.06507f, 0.71579f, 0.86069f),
|
||||
0.0f, 0.05f);
|
||||
} else {
|
||||
crt_.set_fixed_framing(crt_.get_rect_for_area(30, 256, 160, 800));
|
||||
}
|
||||
}
|
||||
|
||||
bool dynamic_framing() const {
|
||||
return dynamic_framing_;
|
||||
}
|
||||
|
||||
void set_palette(const uint8_t value) {
|
||||
@@ -370,8 +466,7 @@ public:
|
||||
}
|
||||
|
||||
void set_control(const uint8_t value) {
|
||||
crtc_clock_multiplier_ = (value & 0x10) ? 1 : 2;
|
||||
|
||||
active_collation_.crtc_clock_multiplier = (value & 0x10) ? 1 : 2;
|
||||
active_collation_.pixels_per_clock = 1 << ((value >> 2) & 0x03);
|
||||
active_collation_.is_teletext = value & 0x02;
|
||||
flash_mask_ = value & 0x01 ? 7 : 0;
|
||||
@@ -388,7 +483,10 @@ public:
|
||||
static_assert(!(PixelAllocationUnit % 16));
|
||||
static_assert(!(PixelAllocationUnit % 12));
|
||||
|
||||
system_via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(state.vsync);
|
||||
if(state.vsync != vsync_) {
|
||||
vsync_receiver_.set_vsync(state.vsync);
|
||||
vsync_ = state.vsync;
|
||||
}
|
||||
|
||||
// Count cycles since horizontal sync to insert a colour burst.
|
||||
// TODO: this is copy/pasted from the CPC. How does the BBC do it?
|
||||
@@ -419,7 +517,7 @@ public:
|
||||
}
|
||||
previous_vsync_ = state.vsync;
|
||||
|
||||
if(state.display_enable && !previous_display_enabled_) {
|
||||
if(state.display_enable && !previous_display_enabled_ && active_collation_.is_teletext) {
|
||||
saa5050_serialiser_.begin_line();
|
||||
}
|
||||
previous_display_enabled_ = state.display_enable;
|
||||
@@ -500,7 +598,7 @@ public:
|
||||
previous_collation_ = active_collation_;
|
||||
|
||||
if(!pixel_data_) {
|
||||
pixel_pointer_ = pixel_data_ = crt_.begin_data(PixelAllocationUnit, 8);
|
||||
pixel_pointer_ = pixel_data_ = crt_.begin_data(PixelAllocationUnit);
|
||||
}
|
||||
|
||||
if(pixel_data_) {
|
||||
@@ -518,7 +616,7 @@ public:
|
||||
pixel_pointer_ += 12;
|
||||
}
|
||||
} else {
|
||||
switch(crtc_clock_multiplier_ * active_collation_.pixels_per_clock) {
|
||||
switch(active_collation_.crtc_clock_multiplier * active_collation_.pixels_per_clock) {
|
||||
case 1: shift_pixels<1>(cursor_shifter_ & 7); break;
|
||||
case 2: shift_pixels<2>(cursor_shifter_ & 7); break;
|
||||
case 4: shift_pixels<4>(cursor_shifter_ & 7); break;
|
||||
@@ -531,7 +629,7 @@ public:
|
||||
}
|
||||
|
||||
// Increment cycles since state changed.
|
||||
cycles_ += crtc_clock_multiplier_ << 3;
|
||||
cycles_ += active_collation_.crtc_clock_multiplier << 3;
|
||||
}
|
||||
|
||||
/// Sets the destination for output.
|
||||
@@ -562,20 +660,30 @@ private:
|
||||
Pixels
|
||||
};
|
||||
struct PixelCollation {
|
||||
int pixels_per_clock;
|
||||
bool is_teletext;
|
||||
int crtc_clock_multiplier = 1;
|
||||
int pixels_per_clock = 4;
|
||||
bool is_teletext = false;
|
||||
|
||||
bool operator !=(const PixelCollation &rhs) {
|
||||
if(is_teletext && rhs.is_teletext) return false;
|
||||
return pixels_per_clock != rhs.pixels_per_clock;
|
||||
// If both are teletext, just inspect the clock multiplier.
|
||||
if(is_teletext && rhs.is_teletext) {
|
||||
return crtc_clock_multiplier != rhs.crtc_clock_multiplier;
|
||||
}
|
||||
|
||||
// If one is teletext but the other isn't, that's a sufficient difference.
|
||||
if(is_teletext != rhs.is_teletext) return true;
|
||||
|
||||
// Compare pixel clock rate.
|
||||
return pixels_per_clock != rhs.pixels_per_clock || crtc_clock_multiplier != rhs.crtc_clock_multiplier;
|
||||
}
|
||||
};
|
||||
|
||||
OutputMode previous_output_mode_ = OutputMode::Sync;
|
||||
int cycles_ = 0;
|
||||
int cycles_into_hsync_ = 0;
|
||||
// int cycles_into_hsync_ = 0;
|
||||
|
||||
Outputs::CRT::CRT crt_;
|
||||
bool dynamic_framing_ = true;
|
||||
|
||||
uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr;
|
||||
size_t pixels_collected() const {
|
||||
@@ -590,7 +698,6 @@ private:
|
||||
std::bitset<16> flash_flags_;
|
||||
uint8_t flash_mask_ = 0;
|
||||
|
||||
int crtc_clock_multiplier_ = 1;
|
||||
PixelCollation active_collation_;
|
||||
uint8_t pixel_shifter_ = 0;
|
||||
|
||||
@@ -614,7 +721,8 @@ private:
|
||||
}
|
||||
|
||||
const uint8_t *const ram_ = nullptr;
|
||||
SystemVIA &system_via_;
|
||||
VSyncReceiver &vsync_receiver_;
|
||||
bool vsync_ = false;
|
||||
|
||||
Mullard::SAA5050Serialiser saa5050_serialiser_;
|
||||
};
|
||||
@@ -624,9 +732,30 @@ using CRTC = Motorola::CRTC::CRTC6845<
|
||||
Motorola::CRTC::CursorType::Native>;
|
||||
}
|
||||
|
||||
template <bool has_1770>
|
||||
// MARK: - Tube.
|
||||
|
||||
template <typename HostT, TubeProcessor tube_processor>
|
||||
struct Tube {
|
||||
using TubeULA = Acorn::Tube::ULA<HostT>;
|
||||
TubeULA ula;
|
||||
Acorn::Tube::Processor<TubeULA, tube_processor> processor;
|
||||
|
||||
Tube(HostT &owner) :
|
||||
ula(owner),
|
||||
processor(ula) {}
|
||||
};
|
||||
|
||||
template <typename HostT>
|
||||
struct Tube<HostT, TubeProcessor::None> {
|
||||
Tube(HostT &) {}
|
||||
};
|
||||
|
||||
// MARK: - ConcreteMachine.
|
||||
|
||||
template <TubeProcessor tube_processor, bool has_1770, bool has_beebsid>
|
||||
class ConcreteMachine:
|
||||
public Activity::Source,
|
||||
public Configurable::Device,
|
||||
public Machine,
|
||||
public MachineTypes::AudioProducer,
|
||||
public MachineTypes::JoystickMachine,
|
||||
@@ -636,6 +765,7 @@ class ConcreteMachine:
|
||||
public MachineTypes::TimedMachine,
|
||||
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
|
||||
public NEC::uPD7002::Delegate,
|
||||
public SystemVIADelegate,
|
||||
public Utility::TypeRecipient<CharacterMapper>,
|
||||
public WD::WD1770::Delegate
|
||||
{
|
||||
@@ -645,13 +775,14 @@ public:
|
||||
const ROMMachine::ROMFetcher &rom_fetcher
|
||||
) :
|
||||
m6502_(*this),
|
||||
system_via_port_handler_(audio_, crtc_bus_handler_, system_via_, joysticks_, target.should_shift_restart),
|
||||
system_via_port_handler_(audio_, crtc_bus_handler_, system_via_, *this, joysticks_, target.should_shift_restart),
|
||||
user_via_(user_via_port_handler_),
|
||||
system_via_(system_via_port_handler_),
|
||||
crtc_bus_handler_(ram_.data(), system_via_),
|
||||
crtc_bus_handler_(ram_.data(), system_via_port_handler_),
|
||||
crtc_(crtc_bus_handler_),
|
||||
acia_(HalfCycles(2'000'000)), // TODO: look up real ACIA clock rate.
|
||||
adc_(HalfCycles(2'000'000))
|
||||
adc_(HalfCycles(2'000'000)),
|
||||
tube_(*this)
|
||||
{
|
||||
set_clock_rate(2'000'000);
|
||||
|
||||
@@ -668,13 +799,17 @@ public:
|
||||
using Name = ::ROM::Name;
|
||||
|
||||
auto request = Request(Name::AcornBASICII) && Request(Name::BBCMicroMOS12);
|
||||
if(target.has_1770dfs) {
|
||||
request = request && Request(Name::BBCMicroDFS226);
|
||||
if(target.has_1770dfs || tube_processor != TubeProcessor::None) {
|
||||
request = request && Request(Name::BBCMicro1770DFS226);
|
||||
}
|
||||
if(target.has_adfs) {
|
||||
request = request && Request(Name::BBCMicroADFS130);
|
||||
}
|
||||
|
||||
if constexpr (tube_processor != TubeProcessor::None) {
|
||||
request = request && Request(tube_.processor.ROM);
|
||||
}
|
||||
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
@@ -686,13 +821,33 @@ public:
|
||||
// Put BASIC in pole position.
|
||||
install_sideways(15, roms.find(Name::AcornBASICII)->second, false);
|
||||
|
||||
// Install filing systems: put the DFS before the ADFS because it's more common on the BBC.
|
||||
// Install filing systems: put the DFS before the ADFS because it's more common on the BBC if the user
|
||||
// explicitly requested DFS. Include it at the end otherwise if it's just implied by the Tube.
|
||||
size_t fs_slot = 14;
|
||||
const auto add_sideways = [&](const Name name) {
|
||||
install_sideways(fs_slot--, roms.find(name)->second, false);
|
||||
};
|
||||
if(target.has_1770dfs) {
|
||||
install_sideways(fs_slot--, roms.find(Name::BBCMicroDFS226)->second, false);
|
||||
add_sideways(Name::BBCMicro1770DFS226);
|
||||
}
|
||||
if(target.has_adfs) {
|
||||
install_sideways(fs_slot--, roms.find(Name::BBCMicroADFS130)->second, false);
|
||||
add_sideways(Name::BBCMicroADFS130);
|
||||
}
|
||||
if(!target.has_1770dfs && tube_processor != TubeProcessor::None) {
|
||||
add_sideways(Name::BBCMicro1770DFS226);
|
||||
}
|
||||
|
||||
// Throw the tube ROM to its target.
|
||||
if constexpr (tube_processor != TubeProcessor::None) {
|
||||
tube_.processor.set_rom(roms.find(tube_.processor.ROM)->second);
|
||||
}
|
||||
|
||||
// Install the ADT ROM if available, but don't error if it's missing. It's very optional.
|
||||
if(target.has_1770dfs || target.has_adfs) {
|
||||
const auto adt_rom = rom_fetcher(Request(Name::BBCMicroAdvancedDiscToolkit140));
|
||||
if(const auto rom = adt_rom.find(Name::BBCMicroAdvancedDiscToolkit140); rom != adt_rom.end()) {
|
||||
install_sideways(fs_slot--, rom->second, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Throw sideways RAM into all unused slots.
|
||||
@@ -722,13 +877,10 @@ public:
|
||||
}
|
||||
|
||||
// MARK: - 6502 bus.
|
||||
Cycles perform_bus_operation(
|
||||
const CPU::MOS6502::BusOperation operation,
|
||||
const uint16_t address,
|
||||
uint8_t *const value
|
||||
) {
|
||||
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
|
||||
Cycles perform(const AddressT address, CPU::MOS6502Mk2::data_t<operation> value) {
|
||||
// Returns @c true if @c address is a device on the 1Mhz bus; @c false otherwise.
|
||||
static constexpr auto is_1mhz = [](const uint16_t address) {
|
||||
const auto is_1mhz = [&] {
|
||||
// Fast exit if outside the IO space.
|
||||
if(address < 0xfc00) return false;
|
||||
if(address >= 0xff00) return false;
|
||||
@@ -747,10 +899,10 @@ public:
|
||||
|
||||
// Otherwise: in IO space, but not a 1Mhz device.
|
||||
return false;
|
||||
};
|
||||
}();
|
||||
|
||||
// Determine whether this access hits the 1Mhz bus; if so then apply appropriate penalty, and update phase.
|
||||
const auto duration = Cycles(is_1mhz(address) ? 2 + (phase_&1) : 1);
|
||||
const auto duration = Cycles(is_1mhz ? 2 + (phase_&1) : 1);
|
||||
if(typer_) typer_->run_for(duration);
|
||||
phase_ += duration.as<int>();
|
||||
|
||||
@@ -777,11 +929,14 @@ public:
|
||||
}
|
||||
adc_.run_for(duration);
|
||||
|
||||
|
||||
if constexpr (has_1770) {
|
||||
// The WD1770 is nominally clocked at 8Mhz.
|
||||
wd1770_.run_for(duration * 4);
|
||||
}
|
||||
if constexpr (requires {tube_.processor;}) {
|
||||
tube_.processor.run_for(duration);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Questionably-clocked devices.
|
||||
@@ -794,109 +949,126 @@ public:
|
||||
//
|
||||
if(address >= 0xfc00 && address < 0xff00) {
|
||||
if(address >= 0xfe40 && address < 0xfe60) {
|
||||
if(is_read(operation)) {
|
||||
*value = system_via_.read(address);
|
||||
if constexpr (is_read(operation)) {
|
||||
value = system_via_.read(address);
|
||||
} else {
|
||||
system_via_.write(address, *value);
|
||||
system_via_.write(address, value);
|
||||
}
|
||||
} else if(address >= 0xfe60 && address < 0xfe80) {
|
||||
if(is_read(operation)) {
|
||||
*value = user_via_.read(address);
|
||||
if constexpr (is_read(operation)) {
|
||||
value = user_via_.read(address);
|
||||
} else {
|
||||
user_via_.write(address, *value);
|
||||
user_via_.write(address, value);
|
||||
}
|
||||
} else if(address == 0xfe30) {
|
||||
if(is_read(operation)) {
|
||||
*value = 0xfe;
|
||||
if constexpr (is_read(operation)) {
|
||||
value = 0xfe;
|
||||
} else {
|
||||
page_sideways(*value & 0xf);
|
||||
page_sideways(value & 0xf);
|
||||
}
|
||||
} else if(address >= 0xfe00 && address < 0xfe08) {
|
||||
if(is_read(operation)) {
|
||||
if constexpr (is_read(operation)) {
|
||||
if(address & 1) {
|
||||
*value = crtc_.get_register();
|
||||
value = crtc_.get_register();
|
||||
} else {
|
||||
*value = crtc_.get_status();
|
||||
value = crtc_.get_status();
|
||||
}
|
||||
} else {
|
||||
if(address & 1) {
|
||||
crtc_.set_register(*value);
|
||||
crtc_.set_register(value);
|
||||
} else {
|
||||
crtc_.select_register(*value);
|
||||
crtc_.select_register(value);
|
||||
}
|
||||
}
|
||||
} else if(address >= 0xfe20 && address < 0xfe30) {
|
||||
if(is_read(operation)) {
|
||||
*value = 0xfe;
|
||||
if constexpr (is_read(operation)) {
|
||||
value = 0xfe;
|
||||
} else {
|
||||
switch(address) {
|
||||
case 0xfe20:
|
||||
crtc_bus_handler_.set_control(*value);
|
||||
crtc_2mhz_ = *value & 0x10;
|
||||
crtc_bus_handler_.set_control(value);
|
||||
crtc_2mhz_ = value & 0x10;
|
||||
break;
|
||||
case 0xfe21:
|
||||
crtc_bus_handler_.set_palette(*value);
|
||||
crtc_bus_handler_.set_palette(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(address == 0xfee0) {
|
||||
if(is_read(operation)) {
|
||||
Logger::info().append("Read tube status: 0");
|
||||
*value = 0;
|
||||
} else if(address >= 0xfee0 && address < 0xfee8) {
|
||||
if constexpr (requires {tube_.ula;}) {
|
||||
if constexpr (is_read(operation)) {
|
||||
value = tube_.ula.host_read(address);
|
||||
} else {
|
||||
tube_.ula.host_write(address, value);
|
||||
}
|
||||
} else {
|
||||
Logger::info().append("Wrote tube: %02x", *value);
|
||||
if constexpr (is_read(operation)) {
|
||||
value = address == 0xfee0 ? 0xfe : 0xff;
|
||||
}
|
||||
}
|
||||
} else if(address >= 0xfe08 && address < 0xfe10) {
|
||||
if(is_read(operation)) {
|
||||
if constexpr (is_read(operation)) {
|
||||
// Logger::info().append("ACIA read");
|
||||
*value = acia_.read(address);
|
||||
value = acia_.read(address);
|
||||
} else {
|
||||
// Logger::info().append("ACIA write: %02x", *value);
|
||||
acia_.write(address, *value);
|
||||
acia_.write(address, value);
|
||||
}
|
||||
} else if(address >= 0xfec0 && address < 0xfee0) {
|
||||
if(is_read(operation)) {
|
||||
*value = adc_.read(address);
|
||||
if constexpr (is_read(operation)) {
|
||||
value = adc_.read(address);
|
||||
} else {
|
||||
adc_.write(address, *value);
|
||||
adc_.write(address, value);
|
||||
}
|
||||
} else if(has_1770 && address >= 0xfe80 && address < 0xfe88) {
|
||||
switch(address) {
|
||||
case 0xfe80:
|
||||
if(!is_read(operation)) {
|
||||
wd1770_.set_control_register(*value);
|
||||
if constexpr (!is_read(operation)) {
|
||||
wd1770_.set_control_register(value);
|
||||
} else {
|
||||
value = 0xff;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if(is_read(operation)) {
|
||||
*value = wd1770_.read(address);
|
||||
if constexpr (is_read(operation)) {
|
||||
value = wd1770_.read(address);
|
||||
} else {
|
||||
wd1770_.write(address, *value);
|
||||
wd1770_.write(address, value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else if(has_beebsid && address >= 0xfc20 && address < 0xfc40) {
|
||||
if constexpr (is_read(operation)) {
|
||||
value = audio_.template get<MOS::SID::SID>().read(+address);
|
||||
} else {
|
||||
audio_.template get<MOS::SID::SID>().write(+address, value);
|
||||
}
|
||||
} else {
|
||||
Logger::error()
|
||||
.append("Unhandled IO %s at %04x", is_read(operation) ? "read" : "write", address)
|
||||
.append_if(!is_read(operation), ": %02x", *value);
|
||||
.append_if(!is_read(operation), ": %02x", value);
|
||||
|
||||
if constexpr (is_read(operation)) {
|
||||
value = 0xff;
|
||||
}
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// ROM or RAM access.
|
||||
//
|
||||
if(is_read(operation)) {
|
||||
if constexpr (is_read(operation)) {
|
||||
// TODO: probably don't do this with this condition? See how it compiles. If it's a CMOV somehow, no problem.
|
||||
if((address >> 14) == 2 && !sideways_read_mask_) {
|
||||
*value = 0xff;
|
||||
value = 0xff;
|
||||
} else {
|
||||
*value = memory_[address >> 14][address];
|
||||
value = memory_[address >> 14][address];
|
||||
}
|
||||
} else {
|
||||
if(memory_write_masks_[address >> 14]) {
|
||||
memory_[address >> 14][address] = *value;
|
||||
memory_[address >> 14][address] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -926,6 +1098,11 @@ private:
|
||||
return crtc_bus_handler_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
// MARK: - SystemVIADelegate.
|
||||
void strobe_lightpen() override {
|
||||
crtc_.trigger_light_pen();
|
||||
}
|
||||
|
||||
// MARK: - KeyboardMachine.
|
||||
BBCMicro::KeyboardMapper mapper_;
|
||||
KeyboardMapper *get_keyboard_mapper() override {
|
||||
@@ -933,20 +1110,48 @@ private:
|
||||
}
|
||||
|
||||
void set_key_state(const uint16_t key, const bool is_pressed) override {
|
||||
if(key == uint16_t(BBCMicro::Key::Break)) {
|
||||
m6502_.set_reset_line(is_pressed);
|
||||
} else {
|
||||
system_via_port_handler_.set_key(uint8_t(key), is_pressed);
|
||||
switch(Key(key)) {
|
||||
case Key::SwitchOffCaps:
|
||||
// Store current caps lock state for a potential restore; press caps lock
|
||||
// now if there's a need to exit caps lock mode.
|
||||
was_caps_ = system_via_port_handler_.caps_lock();
|
||||
if(was_caps_) {
|
||||
system_via_port_handler_.set_key(uint8_t(Key::CapsLock), true);
|
||||
}
|
||||
break;
|
||||
case Key::RestoreCaps:
|
||||
// Press caps lock again if the machine was originally in the caps lock state.
|
||||
// If so then SwitchOffCaps switched it off.
|
||||
if(was_caps_) {
|
||||
system_via_port_handler_.set_key(uint8_t(Key::CapsLock), true);
|
||||
}
|
||||
break;
|
||||
|
||||
case Key::Break:
|
||||
set_reset(is_pressed);
|
||||
break;
|
||||
|
||||
default:
|
||||
system_via_port_handler_.set_key(uint8_t(key), is_pressed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
bool was_caps_ = false;
|
||||
|
||||
void clear_all_keys() final {
|
||||
m6502_.set_reset_line(false);
|
||||
set_reset(false);
|
||||
system_via_port_handler_.clear_all_keys();
|
||||
}
|
||||
|
||||
void set_reset(const bool reset) {
|
||||
m6502_.template set<CPU::MOS6502Mk2::Line::Reset>(reset);
|
||||
if constexpr (requires {tube_.ula;}) {
|
||||
tube_.ula.set_reset(reset);
|
||||
}
|
||||
}
|
||||
|
||||
HalfCycles get_typer_delay(const std::string &text) const final {
|
||||
if(!m6502_.get_is_resetting()) {
|
||||
if(!m6502_.is_resetting()) {
|
||||
return Cycles(0);
|
||||
}
|
||||
|
||||
@@ -966,7 +1171,7 @@ private:
|
||||
Utility::TypeRecipient<CharacterMapper>::add_typer(string);
|
||||
}
|
||||
|
||||
bool can_type(char c) const final {
|
||||
bool can_type(const char c) const final {
|
||||
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
|
||||
}
|
||||
|
||||
@@ -976,7 +1181,10 @@ private:
|
||||
}
|
||||
|
||||
void flush_output(const int outputs) final {
|
||||
if(outputs & Output::Audio) {
|
||||
// TODO: I think there's an infrastructural bug here on macOS; if the audio output has stalled out,
|
||||
// the outer wrapper won't ask for an audio flush, which means the queue will never try to start,
|
||||
// and the audio queue will just fill indefinitely. Could this be the mythical 'leak'?
|
||||
if(outputs & Output::Audio || audio_.queue_size() > 200) {
|
||||
audio_.flush();
|
||||
}
|
||||
}
|
||||
@@ -988,7 +1196,7 @@ private:
|
||||
|
||||
// MARK: - uPD7002::Delegate.
|
||||
void did_change_interrupt_status(NEC::uPD7002 &) override {
|
||||
system_via_.set_control_line_input<MOS::MOS6522::Port::B, MOS::MOS6522::Line::One>(adc_.interrupt());
|
||||
system_via_.template set_control_line_input<MOS::MOS6522::Port::B, MOS::MOS6522::Line::One>(adc_.interrupt());
|
||||
}
|
||||
|
||||
// MARK: - MediaTarget.
|
||||
@@ -1029,26 +1237,46 @@ private:
|
||||
rom_write_masks_[slot] = is_writeable;
|
||||
rom_inserted_[slot] = true;
|
||||
|
||||
assert(source.size() == roms_[slot].size());
|
||||
std::copy(source.begin(), source.end(), roms_[slot].begin());
|
||||
assert(roms_[slot].size() % source.size() == 0);
|
||||
auto begin = roms_[slot].begin();
|
||||
while(begin != roms_[slot].end()) {
|
||||
std::copy(source.begin(), source.end(), begin);
|
||||
std::advance(begin, source.size());
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Components.
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
|
||||
|
||||
struct M6502Traits {
|
||||
static constexpr auto uses_ready_line = false;
|
||||
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::BetweenInstructions;
|
||||
using BusHandlerT = ConcreteMachine;
|
||||
};
|
||||
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
|
||||
|
||||
UserVIAPortHandler user_via_port_handler_;
|
||||
SystemVIAPortHandler system_via_port_handler_;
|
||||
SystemVIAPortHandler<Audio<has_beebsid>> system_via_port_handler_;
|
||||
UserVIA user_via_;
|
||||
SystemVIA system_via_;
|
||||
MOS::MOS6522::MOS6522<SystemVIAPortHandler<Audio<has_beebsid>>> system_via_;
|
||||
|
||||
void update_irq_line() {
|
||||
m6502_.set_irq_line(
|
||||
const bool tube_irq =
|
||||
[&] {
|
||||
if constexpr (requires {tube_.ula;}) {
|
||||
return tube_.ula.has_host_irq();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} ();
|
||||
|
||||
m6502_.template set<CPU::MOS6502Mk2::Line::IRQ>(
|
||||
user_via_.get_interrupt_line() ||
|
||||
system_via_.get_interrupt_line()
|
||||
system_via_.get_interrupt_line() ||
|
||||
tube_irq
|
||||
);
|
||||
}
|
||||
|
||||
Audio audio_;
|
||||
Audio<has_beebsid> audio_;
|
||||
|
||||
CRTCBusHandler crtc_bus_handler_;
|
||||
CRTC crtc_;
|
||||
@@ -1061,7 +1289,9 @@ private:
|
||||
// MARK: - WD1770.
|
||||
Electron::Plus3 wd1770_;
|
||||
void wd1770_did_change_output(WD::WD1770 &) override {
|
||||
m6502_.set_nmi_line(wd1770_.get_interrupt_request_line() || wd1770_.get_data_request_line());
|
||||
m6502_.template set<CPU::MOS6502Mk2::Line::NMI>(
|
||||
wd1770_.get_interrupt_request_line() || wd1770_.get_data_request_line()
|
||||
);
|
||||
}
|
||||
|
||||
// MARK: - Joysticks
|
||||
@@ -1069,21 +1299,65 @@ private:
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->dynamic_crop = crtc_bus_handler_.dynamic_framing();
|
||||
return options;
|
||||
}
|
||||
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
crtc_bus_handler_.set_dynamic_framing(options->dynamic_crop);
|
||||
}
|
||||
|
||||
// MARK: - Tube.
|
||||
|
||||
Tube<ConcreteMachine, tube_processor> tube_;
|
||||
|
||||
public:
|
||||
void set_host_tube_irq(bool) { update_irq_line(); }
|
||||
void set_parasite_tube_irq(const bool active) { tube_.processor.set_irq(active); }
|
||||
void set_parasite_tube_nmi(const bool active) { tube_.processor.set_nmi(active); }
|
||||
void set_parasite_reset(const bool active) { tube_.processor.set_reset(active); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
using namespace BBCMicro;
|
||||
|
||||
namespace {
|
||||
using Target = Analyser::Static::Acorn::BBCMicroTarget;
|
||||
|
||||
template <Target::TubeProcessor processor, bool has_1770>
|
||||
std::unique_ptr<Machine> machine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
if(target.has_beebsid) {
|
||||
return std::make_unique<BBCMicro::ConcreteMachine<processor, has_1770, true>>(target, rom_fetcher);
|
||||
} else {
|
||||
return std::make_unique<BBCMicro::ConcreteMachine<processor, has_1770, false>>(target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
|
||||
template <Target::TubeProcessor processor>
|
||||
std::unique_ptr<Machine> machine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
if(target.has_1770dfs || target.has_adfs) {
|
||||
return machine<processor, true>(target, rom_fetcher);
|
||||
} else {
|
||||
return machine<processor, false>(target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Machine> Machine::BBCMicro(
|
||||
const Analyser::Static::Target *target,
|
||||
const ROMMachine::ROMFetcher &rom_fetcher
|
||||
) {
|
||||
using Target = Analyser::Static::Acorn::BBCMicroTarget;
|
||||
const Target *const acorn_target = dynamic_cast<const Target *>(target);
|
||||
if(acorn_target->has_1770dfs || acorn_target->has_adfs) {
|
||||
return std::make_unique<BBCMicro::ConcreteMachine<true>>(*acorn_target, rom_fetcher);
|
||||
} else {
|
||||
return std::make_unique<BBCMicro::ConcreteMachine<false>>(*acorn_target, rom_fetcher);
|
||||
switch(acorn_target->tube_processor) {
|
||||
case TubeProcessor::None: return machine<TubeProcessor::None>(*acorn_target, rom_fetcher);
|
||||
case TubeProcessor::WDC65C02: return machine<TubeProcessor::WDC65C02>(*acorn_target, rom_fetcher);
|
||||
case TubeProcessor::Z80: return machine<TubeProcessor::Z80>(*acorn_target, rom_fetcher);
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "Configurable/Configurable.hpp"
|
||||
#include "Configurable/StandardOptions.hpp"
|
||||
#include "Machines/ROMMachine.hpp"
|
||||
|
||||
#include <memory>
|
||||
@@ -22,6 +24,25 @@ struct Machine {
|
||||
const Analyser::Static::Target *target,
|
||||
const ROMMachine::ROMFetcher &rom_fetcher
|
||||
);
|
||||
|
||||
class Options:
|
||||
public Reflection::StructImpl<Options>,
|
||||
public Configurable::Options::DynamicCrop<Options>
|
||||
{
|
||||
public:
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::Options::DynamicCrop<Options>(type == Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
private:
|
||||
friend Configurable::Options::DynamicCrop<Options>;
|
||||
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
declare_dynamic_crop_option();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -43,23 +43,29 @@ enum class Key: uint16_t {
|
||||
ForwardSlash = 0x68,
|
||||
Bit1 = 0x08,
|
||||
|
||||
Right = 0x79, Left = 0x19, Down = 0x29, Up = 0x39,
|
||||
Return = 0x49, Delete = 0x59, Copy = 0x69, Bit0 = 0x09,
|
||||
Right = 0x79, Left = 0x19, Down = 0x29, Up = 0x39,
|
||||
Return = 0x49, Delete = 0x59, Copy = 0x69, Bit0 = 0x09,
|
||||
|
||||
//
|
||||
// Break; a key, but not on the keyboard matrix.
|
||||
//
|
||||
Break = 0xfffc,
|
||||
Break = 0xfe00,
|
||||
|
||||
//
|
||||
// Fictional keys to aid key entry.
|
||||
//
|
||||
SwitchOffCaps = 0xfe01,
|
||||
RestoreCaps = 0xfe02,
|
||||
|
||||
//
|
||||
// Master only keys.
|
||||
//
|
||||
Keypad4 = 0x7a, Keypad6 = 0x1a, Keypad8 = 0x2a, KeypadPlus = 0x3a,
|
||||
KeypadDivide = 0x4a, KeypadHash = 0x5a, Keypad0 = 0x6a,
|
||||
Keypad5 = 0x7b, Keypad7 = 0x1b, Keypad9 = 0x2b, KeypadMinus = 0x3b,
|
||||
KeypadDeleted = 0x4b, KeypadMultiply = 0x5b, Keypad1 = 0x6b,
|
||||
Keypad2 = 0x7c, F11 = 0x1c, PauseBreak = 0x2c, KeypadReturn = 0x3c,
|
||||
KeypadDot = 0x4c, KeypadComma = 0x5c, Keypad3 = 0x6c,
|
||||
Keypad4 = 0x7a, Keypad6 = 0x1a, Keypad8 = 0x2a, KeypadPlus = 0x3a,
|
||||
KeypadDivide = 0x4a, KeypadHash = 0x5a, Keypad0 = 0x6a,
|
||||
Keypad5 = 0x7b, Keypad7 = 0x1b, Keypad9 = 0x2b, KeypadMinus = 0x3b,
|
||||
KeypadDeleted = 0x4b, KeypadMultiply = 0x5b, Keypad1 = 0x6b,
|
||||
Keypad2 = 0x7c, F11 = 0x1c, PauseBreak = 0x2c, KeypadReturn = 0x3c,
|
||||
KeypadDot = 0x4c, KeypadComma = 0x5c, Keypad3 = 0x6c,
|
||||
|
||||
Alt = 0x02,
|
||||
LeftShift = 0x03,
|
||||
@@ -162,33 +168,36 @@ private:
|
||||
}
|
||||
|
||||
static inline const std::unordered_map<char, Sequence> sequences = {
|
||||
{'Q', keys({Key::Q}) }, {'W', keys({Key::W}) },
|
||||
{'E', keys({Key::E}) }, {'R', keys({Key::R}) },
|
||||
{'T', keys({Key::T}) }, {'Y', keys({Key::Y}) },
|
||||
{'U', keys({Key::U}) }, {'I', keys({Key::I}) },
|
||||
{'O', keys({Key::O}) }, {'P', keys({Key::P}) },
|
||||
{'A', keys({Key::A}) }, {'S', keys({Key::S}) },
|
||||
{'D', keys({Key::D}) }, {'F', keys({Key::F}) },
|
||||
{'G', keys({Key::G}) }, {'H', keys({Key::H}) },
|
||||
{'J', keys({Key::J}) }, {'K', keys({Key::K}) },
|
||||
{'L', keys({Key::L}) }, {'Z', keys({Key::Z}) },
|
||||
{'X', keys({Key::X}) }, {'C', keys({Key::C}) },
|
||||
{'V', keys({Key::V}) }, {'B', keys({Key::B}) },
|
||||
{'N', keys({Key::N}) }, {'M', keys({Key::M}) },
|
||||
{Utility::Typer::BeginString, keys({Key::SwitchOffCaps})},
|
||||
{Utility::Typer::EndString, keys({Key::RestoreCaps})},
|
||||
|
||||
{'q', keys({Key::Shift, Key::Q}) }, {'w', keys({Key::Shift, Key::W}) },
|
||||
{'e', keys({Key::Shift, Key::E}) }, {'r', keys({Key::Shift, Key::R}) },
|
||||
{'t', keys({Key::Shift, Key::T}) }, {'y', keys({Key::Shift, Key::Y}) },
|
||||
{'u', keys({Key::Shift, Key::U}) }, {'i', keys({Key::Shift, Key::I}) },
|
||||
{'o', keys({Key::Shift, Key::O}) }, {'p', keys({Key::Shift, Key::P}) },
|
||||
{'a', keys({Key::Shift, Key::A}) }, {'s', keys({Key::Shift, Key::S}) },
|
||||
{'d', keys({Key::Shift, Key::D}) }, {'f', keys({Key::Shift, Key::F}) },
|
||||
{'g', keys({Key::Shift, Key::G}) }, {'h', keys({Key::Shift, Key::H}) },
|
||||
{'j', keys({Key::Shift, Key::J}) }, {'k', keys({Key::Shift, Key::K}) },
|
||||
{'l', keys({Key::Shift, Key::L}) }, {'z', keys({Key::Shift, Key::Z}) },
|
||||
{'x', keys({Key::Shift, Key::X}) }, {'c', keys({Key::Shift, Key::C}) },
|
||||
{'v', keys({Key::Shift, Key::V}) }, {'b', keys({Key::Shift, Key::B}) },
|
||||
{'n', keys({Key::Shift, Key::N}) }, {'m', keys({Key::Shift, Key::M}) },
|
||||
{'q', keys({Key::Q}) }, {'w', keys({Key::W}) },
|
||||
{'e', keys({Key::E}) }, {'r', keys({Key::R}) },
|
||||
{'t', keys({Key::T}) }, {'y', keys({Key::Y}) },
|
||||
{'u', keys({Key::U}) }, {'i', keys({Key::I}) },
|
||||
{'o', keys({Key::O}) }, {'p', keys({Key::P}) },
|
||||
{'a', keys({Key::A}) }, {'s', keys({Key::S}) },
|
||||
{'d', keys({Key::D}) }, {'f', keys({Key::F}) },
|
||||
{'g', keys({Key::G}) }, {'h', keys({Key::H}) },
|
||||
{'j', keys({Key::J}) }, {'k', keys({Key::K}) },
|
||||
{'l', keys({Key::L}) }, {'z', keys({Key::Z}) },
|
||||
{'x', keys({Key::X}) }, {'c', keys({Key::C}) },
|
||||
{'v', keys({Key::V}) }, {'b', keys({Key::B}) },
|
||||
{'n', keys({Key::N}) }, {'m', keys({Key::M}) },
|
||||
|
||||
{'Q', keys({Key::Shift, Key::Q}) }, {'W', keys({Key::Shift, Key::W}) },
|
||||
{'E', keys({Key::Shift, Key::E}) }, {'R', keys({Key::Shift, Key::R}) },
|
||||
{'T', keys({Key::Shift, Key::T}) }, {'Y', keys({Key::Shift, Key::Y}) },
|
||||
{'U', keys({Key::Shift, Key::U}) }, {'I', keys({Key::Shift, Key::I}) },
|
||||
{'O', keys({Key::Shift, Key::O}) }, {'P', keys({Key::Shift, Key::P}) },
|
||||
{'A', keys({Key::Shift, Key::A}) }, {'S', keys({Key::Shift, Key::S}) },
|
||||
{'D', keys({Key::Shift, Key::D}) }, {'F', keys({Key::Shift, Key::F}) },
|
||||
{'G', keys({Key::Shift, Key::G}) }, {'H', keys({Key::Shift, Key::H}) },
|
||||
{'J', keys({Key::Shift, Key::J}) }, {'K', keys({Key::Shift, Key::K}) },
|
||||
{'L', keys({Key::Shift, Key::L}) }, {'Z', keys({Key::Shift, Key::Z}) },
|
||||
{'X', keys({Key::Shift, Key::X}) }, {'C', keys({Key::Shift, Key::C}) },
|
||||
{'V', keys({Key::Shift, Key::V}) }, {'B', keys({Key::Shift, Key::B}) },
|
||||
{'N', keys({Key::Shift, Key::N}) }, {'M', keys({Key::Shift, Key::M}) },
|
||||
|
||||
{'0', keys({Key::k0}) }, {'1', keys({Key::k1}) },
|
||||
{'2', keys({Key::k2}) }, {'3', keys({Key::k3}) },
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#include "ClockReceiver/ClockReceiver.hpp"
|
||||
#include "ClockReceiver/ForceInline.hpp"
|
||||
#include "Configurable/StandardOptions.hpp"
|
||||
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "Processors/6502/6502.hpp"
|
||||
|
||||
#include "Storage/MassStorage/SCSI/SCSI.hpp"
|
||||
@@ -27,6 +26,8 @@
|
||||
|
||||
#include "ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
|
||||
#include "Interrupts.hpp"
|
||||
#include "Keyboard.hpp"
|
||||
#include "Plus3.hpp"
|
||||
@@ -58,8 +59,11 @@ public:
|
||||
hard_drive_(scsi_bus_, 0),
|
||||
scsi_device_(scsi_bus_.add_device()),
|
||||
video_(ram_),
|
||||
sound_generator_(audio_queue_),
|
||||
speaker_(sound_generator_) {
|
||||
audio_(
|
||||
2000000.0 / SoundGenerator::clock_rate_divider,
|
||||
SoundGenerator::clock_rate_divider,
|
||||
6000.0f
|
||||
) {
|
||||
memset(key_states_, 0, sizeof(key_states_));
|
||||
for(int c = 0; c < 16; c++)
|
||||
memset(roms_[c], 0xff, 16384);
|
||||
@@ -67,9 +71,6 @@ public:
|
||||
tape_.set_delegate(this);
|
||||
set_clock_rate(2000000);
|
||||
|
||||
speaker_.set_input_rate(2000000 / SoundGenerator::clock_rate_divider);
|
||||
speaker_.set_high_frequency_cutoff(6000);
|
||||
|
||||
::ROM::Request request = ::ROM::Request(::ROM::Name::AcornBASICII) && ::ROM::Request(::ROM::Name::AcornElectronMOS100);
|
||||
if(target.has_pres_adfs) {
|
||||
request = request && ::ROM::Request(::ROM::Name::PRESADFSSlot1) && ::ROM::Request(::ROM::Name::PRESADFSSlot2);
|
||||
@@ -143,7 +144,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
audio_.stop();
|
||||
}
|
||||
|
||||
void set_key_state(uint16_t key, bool isPressed) final {
|
||||
@@ -234,8 +235,7 @@ public:
|
||||
const auto [cycles, video_interrupts] = run_for_access(address);
|
||||
signal_interrupt(video_interrupts);
|
||||
|
||||
cycles_since_audio_update_ += cycles;
|
||||
if(cycles_since_audio_update_ > Cycles(16384)) update_audio();
|
||||
audio_ += cycles;
|
||||
tape_.run_for(cycles);
|
||||
|
||||
if(typer_) typer_->run_for(cycles);
|
||||
@@ -278,8 +278,7 @@ public:
|
||||
// update speaker mode
|
||||
bool new_speaker_is_enabled = (*value & 6) == 2;
|
||||
if(new_speaker_is_enabled != speaker_is_enabled_) {
|
||||
update_audio();
|
||||
sound_generator_.set_is_enabled(new_speaker_is_enabled);
|
||||
audio_->set_is_enabled(new_speaker_is_enabled);
|
||||
speaker_is_enabled_ = new_speaker_is_enabled;
|
||||
}
|
||||
|
||||
@@ -340,8 +339,7 @@ public:
|
||||
break;
|
||||
case 0xfe06:
|
||||
if(!is_read(operation)) {
|
||||
update_audio();
|
||||
sound_generator_.set_divider(*value);
|
||||
audio_->set_divider(*value);
|
||||
tape_.set_counter(*value);
|
||||
}
|
||||
break;
|
||||
@@ -510,8 +508,7 @@ public:
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
audio_.perform();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,7 +529,7 @@ public:
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
return &audio_.speaker();
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) final {
|
||||
@@ -599,7 +596,7 @@ public:
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->output = get_video_signal_configurable();
|
||||
options->quickload = allow_fast_tape_hack_;
|
||||
options->quick_load = allow_fast_tape_hack_;
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -607,7 +604,7 @@ public:
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
|
||||
set_video_signal_configurable(options->output);
|
||||
allow_fast_tape_hack_ = options->quickload;
|
||||
allow_fast_tape_hack_ = options->quick_load;
|
||||
set_use_fast_tape_hack();
|
||||
}
|
||||
|
||||
@@ -682,10 +679,6 @@ private:
|
||||
}
|
||||
|
||||
// MARK: - Work deferral updates.
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(SoundGenerator::clock_rate_divider)));
|
||||
}
|
||||
|
||||
inline void signal_interrupt(uint8_t interrupt) {
|
||||
if(!interrupt) {
|
||||
return;
|
||||
@@ -732,9 +725,6 @@ private:
|
||||
uint8_t key_states_[14];
|
||||
Electron::KeyboardMapper keyboard_mapper_;
|
||||
|
||||
// Counters related to simultaneous subsystems
|
||||
Cycles cycles_since_audio_update_ = 0;
|
||||
|
||||
// Tape
|
||||
Tape tape_;
|
||||
bool use_fast_tape_hack_ = false;
|
||||
@@ -770,10 +760,7 @@ private:
|
||||
|
||||
// Outputs
|
||||
VideoOutput video_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
SoundGenerator sound_generator_;
|
||||
Outputs::Speaker::PullLowpass<SoundGenerator> speaker_;
|
||||
Outputs::Speaker::PullLowpassSpeakerQueue<Cycles, SoundGenerator> audio_;
|
||||
|
||||
bool speaker_is_enabled_ = false;
|
||||
|
||||
|
||||
@@ -35,22 +35,22 @@ struct Machine {
|
||||
/// Defines the runtime options available for an Electron.
|
||||
class Options:
|
||||
public Reflection::StructImpl<Options>,
|
||||
public Configurable::DisplayOption<Options>,
|
||||
public Configurable::QuickloadOption<Options>
|
||||
public Configurable::Options::Display<Options>,
|
||||
public Configurable::Options::QuickLoad<Options>
|
||||
{
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
friend Configurable::QuickloadOption<Options>;
|
||||
friend Configurable::Options::Display<Options>;
|
||||
friend Configurable::Options::QuickLoad<Options>;
|
||||
public:
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(
|
||||
Configurable::Options::Display<Options>(
|
||||
type == Configurable::OptionsType::UserFriendly ?
|
||||
Configurable::Display::RGB : Configurable::Display::CompositeColour
|
||||
),
|
||||
Configurable::QuickloadOption<Options>(
|
||||
Configurable::Options::QuickLoad<Options>(
|
||||
type == Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
|
||||
@@ -30,9 +30,8 @@ enum Key: uint16_t {
|
||||
KeyShift = 0x00d0 | 0x08, KeyControl = 0x00d0 | 0x04, KeyFunc = 0x00d0 | 0x02, KeyEscape = 0x00d0 | 0x01,
|
||||
|
||||
// Virtual keys.
|
||||
KeyF1 = 0xfff0, KeyF2, KeyF3, KeyF4, KeyF5, KeyF6, KeyF7, KeyF8, KeyF9, KeyF0,
|
||||
|
||||
KeyBreak = 0xfffc,
|
||||
KeyF1 = 0xfe00, KeyF2, KeyF3, KeyF4, KeyF5, KeyF6, KeyF7, KeyF8, KeyF9, KeyF0,
|
||||
KeyBreak,
|
||||
};
|
||||
|
||||
constexpr bool is_modifier(const Key key) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
using namespace Electron;
|
||||
|
||||
SoundGenerator::SoundGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
SoundGenerator::SoundGenerator(Outputs::Speaker::TaskQueue &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
void SoundGenerator::set_sample_volume_range(std::int16_t range) {
|
||||
@@ -40,13 +40,13 @@ template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Mix>(std::
|
||||
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
|
||||
void SoundGenerator::set_divider(uint8_t divider) {
|
||||
void SoundGenerator::set_divider(const uint8_t divider) {
|
||||
audio_queue_.enqueue([this, divider]() {
|
||||
divider_ = divider * 32 / clock_rate_divider;
|
||||
});
|
||||
}
|
||||
|
||||
void SoundGenerator::set_is_enabled(bool is_enabled) {
|
||||
void SoundGenerator::set_is_enabled(const bool is_enabled) {
|
||||
audio_queue_.enqueue([this, is_enabled]() {
|
||||
is_enabled_ = is_enabled;
|
||||
counter_ = 0;
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
#include "Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "Outputs/Speaker/SpeakerQueue.hpp"
|
||||
|
||||
namespace Electron {
|
||||
|
||||
class SoundGenerator: public ::Outputs::Speaker::BufferSource<SoundGenerator, false> {
|
||||
public:
|
||||
SoundGenerator(Concurrency::AsyncTaskQueue<false> &);
|
||||
SoundGenerator(Outputs::Speaker::TaskQueue &);
|
||||
|
||||
void set_divider(uint8_t);
|
||||
void set_is_enabled(bool);
|
||||
@@ -28,7 +28,7 @@ public:
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Outputs::Speaker::TaskQueue &audio_queue_;
|
||||
unsigned int counter_ = 0;
|
||||
unsigned int divider_ = 0;
|
||||
bool is_enabled_ = false;
|
||||
|
||||
@@ -20,13 +20,11 @@ VideoOutput::VideoOutput(const uint8_t *memory) :
|
||||
1,
|
||||
Outputs::Display::Type::PAL50,
|
||||
Outputs::Display::InputDataType::Red1Green1Blue1) {
|
||||
crt_.set_visible_area(crt_.get_rect_for_area(
|
||||
312 - vsync_end,
|
||||
256,
|
||||
h_total - hsync_start,
|
||||
80 * 8,
|
||||
4.0f / 3.0f
|
||||
));
|
||||
// Default construction values leave this out of text mode, and text
|
||||
// mode uses a subregion of pixel modes.
|
||||
crt_.set_fixed_framing([&] {
|
||||
run_for(Cycles(10'000));
|
||||
});
|
||||
}
|
||||
|
||||
void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *const scan_target) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Header.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 30/10/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Acorn::Tube {
|
||||
|
||||
template <size_t length, typename ULAT>
|
||||
struct FIFO {
|
||||
FIFO(ULAT &ula, const uint8_t mask = 0x00) : ula_(ula), mask_(mask) {}
|
||||
|
||||
/// @returns b7 set exactly if this FIFO is not empty.
|
||||
uint8_t data_available() const {
|
||||
return read_ != write_ ? 0x80 : 0x00;
|
||||
}
|
||||
|
||||
/// @returns b6 set exactly if this FIFO is not full.
|
||||
uint8_t not_full() const {
|
||||
return size_t(write_ - read_) < length ? 0x40 : 0x00;
|
||||
}
|
||||
|
||||
/// Adds a value to the FIFO, notifying the ULA if this would cause the FIFO to
|
||||
/// transition from empty to not-empty. The ULA will be supplied with this FIFO's
|
||||
/// mask as specified at construction.
|
||||
void write(const uint8_t value) {
|
||||
if(write_ - read_ == length) return;
|
||||
if(write_ == read_) {
|
||||
ula_.fifo_has_data(mask_);
|
||||
}
|
||||
buffer_[(write_++) % length] = value;
|
||||
}
|
||||
|
||||
/// Removes a value from the FIFO.
|
||||
uint8_t read() {
|
||||
const uint8_t result = buffer_[read_ % length];
|
||||
if(write_ != read_) ++read_;
|
||||
if(write_ == read_) {
|
||||
ula_.fifo_is_empty(mask_);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Empties the FIFO.
|
||||
void reset() {
|
||||
read_ = write_ = 0;
|
||||
ula_.fifo_is_empty(mask_);
|
||||
}
|
||||
|
||||
private:
|
||||
ULAT &ula_;
|
||||
uint8_t mask_;
|
||||
std::array<uint8_t, length> buffer_;
|
||||
uint32_t read_ = 0;
|
||||
uint32_t write_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// Tube6502.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 03/11/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "TubeProcessor.hpp"
|
||||
|
||||
#include "Processors/6502Mk2/6502Mk2.hpp"
|
||||
#include "Machines/Utility/ROMCatalogue.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Acorn::Tube {
|
||||
|
||||
template <typename ULAT>
|
||||
class Processor<ULAT, TubeProcessor::WDC65C02> {
|
||||
public:
|
||||
static constexpr auto ROM = ROM::Name::BBCMicro6502Tube110;
|
||||
void set_rom(std::vector<uint8_t> source) {
|
||||
source.resize(sizeof(rom_));
|
||||
std::copy(source.begin(), source.end(), rom_);
|
||||
}
|
||||
|
||||
Processor(ULAT &ula) : m6502_(*this), ula_(ula) {}
|
||||
|
||||
// By convention, these are cycles relative to the host's 2Mhz bus.
|
||||
// Multiply by 3/2 to turn that into the tube 6502's usual 3Mhz bus.
|
||||
void run_for(const Cycles cycles) {
|
||||
cycles_modulo_ += cycles * 3;
|
||||
m6502_.run_for(cycles_modulo_.divide(Cycles(2)));
|
||||
}
|
||||
|
||||
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
|
||||
Cycles perform(const AddressT address, CPU::MOS6502Mk2::data_t<operation> value) {
|
||||
if(address >= 0xfef8 && address < 0xff00) {
|
||||
rom_visible_ = false;
|
||||
if constexpr (is_read(operation)) {
|
||||
value = ula_.parasite_read(address);
|
||||
} else {
|
||||
ula_.parasite_write(address, value);
|
||||
}
|
||||
} else {
|
||||
if constexpr (is_read(operation)) {
|
||||
constexpr uint16_t RomStart = sizeof(ram_) - sizeof(rom_);
|
||||
value = rom_visible_ && address >= RomStart ? rom_[address - RomStart] : ram_[address];
|
||||
} else {
|
||||
ram_[address] = value;
|
||||
}
|
||||
}
|
||||
return Cycles(1);
|
||||
}
|
||||
|
||||
void set_irq(const bool active) { m6502_.template set<CPU::MOS6502Mk2::Line::IRQ>(active); }
|
||||
void set_nmi(const bool active) { m6502_.template set<CPU::MOS6502Mk2::Line::NMI>(active); }
|
||||
void set_reset(const bool reset) {
|
||||
m6502_.template set<CPU::MOS6502Mk2::Line::Reset>(reset);
|
||||
rom_visible_ |= reset;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t rom_[2048];
|
||||
uint8_t ram_[65536];
|
||||
Cycles cycles_modulo_;
|
||||
|
||||
struct M6502Traits {
|
||||
static constexpr auto uses_ready_line = false;
|
||||
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::AnyCycle;
|
||||
using BusHandlerT = Processor;
|
||||
};
|
||||
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::WDC65C02, M6502Traits> m6502_;
|
||||
bool rom_visible_ = true;
|
||||
|
||||
ULAT &ula_;
|
||||
};
|
||||
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// TubeProcessor.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/11/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Analyser/Static/Acorn/Target.hpp"
|
||||
|
||||
namespace Acorn::Tube {
|
||||
|
||||
using TubeProcessor = Analyser::Static::Acorn::BBCMicroTarget::TubeProcessor;
|
||||
template <typename ULAT, TubeProcessor> class Processor;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// TubeZ80.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/11/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "TubeProcessor.hpp"
|
||||
|
||||
#include "Processors/Z80/Z80.hpp"
|
||||
#include "Machines/Utility/ROMCatalogue.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Acorn::Tube {
|
||||
|
||||
template <typename ULAT>
|
||||
class Processor<ULAT, TubeProcessor::Z80>: public CPU::Z80::BusHandler {
|
||||
public:
|
||||
static constexpr auto ROM = ROM::Name::BBCMicroZ80Tube122;
|
||||
void set_rom(std::vector<uint8_t> rom) {
|
||||
rom.resize(sizeof(rom_));
|
||||
std::copy(rom.begin(), rom.end(), std::begin(rom_));
|
||||
}
|
||||
|
||||
Processor(ULAT &ula) : z80_(*this), ula_(ula) {}
|
||||
|
||||
void run_for(const Cycles cycles) {
|
||||
// Map from 2Mhz to 6Mhz.
|
||||
z80_.run_for(cycles * 3);
|
||||
}
|
||||
|
||||
void set_irq(const bool active) { z80_.set_interrupt_line(active); }
|
||||
void set_nmi(const bool active) { z80_.set_non_maskable_interrupt_line(active); }
|
||||
void set_reset(const bool reset) {
|
||||
z80_.set_reset_line(reset);
|
||||
rom_visible_ |= reset;
|
||||
}
|
||||
|
||||
HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
if(!cycle.is_terminal()) {
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
const uint16_t address = *cycle.address;
|
||||
switch(cycle.operation) {
|
||||
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
||||
if(address == 0x66) {
|
||||
rom_visible_ = true;
|
||||
}
|
||||
rom_visible_ &= address < 0x8000;
|
||||
[[fallthrough]];
|
||||
case CPU::Z80::PartialMachineCycle::Read:
|
||||
if(rom_visible_ && address <= sizeof(rom_)) {
|
||||
*cycle.value = rom_[address];
|
||||
return HalfCycles(2);
|
||||
} else {
|
||||
*cycle.value = ram_[address];
|
||||
}
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Write:
|
||||
ram_[address] = *cycle.value;
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Interrupt:
|
||||
*cycle.value = 0xfe;
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
*cycle.value = ula_.parasite_read(address);
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Output:
|
||||
ula_.parasite_write(address, *cycle.value);
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
CPU::Z80::Processor<Processor, false, false> z80_;
|
||||
bool rom_visible_ = true;
|
||||
|
||||
uint8_t rom_[4096];
|
||||
uint8_t ram_[65536];
|
||||
ULAT &ula_;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//
|
||||
// ULA.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 03/11/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "FIFO.hpp"
|
||||
|
||||
namespace Acorn::Tube {
|
||||
|
||||
/*!
|
||||
The non-FIFO section of the tube ULA.
|
||||
*/
|
||||
template <typename HostT>
|
||||
struct ULA {
|
||||
ULA(HostT &host) :
|
||||
host_(host),
|
||||
to_parasite1_(*this, 0x02),
|
||||
to_parasite2_(*this),
|
||||
to_parasite3_(*this, 0x08),
|
||||
to_parasite4_(*this, 0x04),
|
||||
to_host1_(*this),
|
||||
to_host2_(*this),
|
||||
to_host3_(*this),
|
||||
to_host4_(*this, 0x01)
|
||||
{}
|
||||
|
||||
/// Call-in for the FIFOs; indicates that a FIFO just went from empty to not-empty,
|
||||
/// which might cause an interrupt elsewhere depending on the mask and on whether
|
||||
/// that interrupt is enabled.
|
||||
void fifo_has_data(const uint8_t mask) {
|
||||
apply_fifo_mask(mask, 0xff);
|
||||
}
|
||||
|
||||
void fifo_is_empty(const uint8_t mask) {
|
||||
apply_fifo_mask(0x00, ~mask);
|
||||
}
|
||||
|
||||
bool has_host_irq() const {
|
||||
return (flags_ & 0x01) && to_host4_.data_available();
|
||||
}
|
||||
|
||||
bool has_parasite_irq() const {
|
||||
return
|
||||
((flags_ & 0x02) && to_parasite1_.data_available()) ||
|
||||
((flags_ & 0x04) && to_parasite4_.data_available());
|
||||
}
|
||||
|
||||
bool has_parasite_nmi() const {
|
||||
return (flags_ & 0x08) && to_parasite3_.data_available();
|
||||
}
|
||||
|
||||
void parasite_write(const uint16_t address, const uint8_t value) {
|
||||
switch(address & 7) {
|
||||
case 1: to_host1_.write(value); break;
|
||||
case 3: to_host2_.write(value); break;
|
||||
case 5: to_host3_.write(value); break;
|
||||
case 7: to_host4_.write(value); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t parasite_read(const uint16_t address) {
|
||||
switch(address & 7) {
|
||||
case 0: return to_parasite1_.data_available() | to_host1_.not_full() | status();
|
||||
case 1: return to_parasite1_.read();
|
||||
case 2: return to_parasite2_.data_available() | to_host2_.not_full();
|
||||
case 3: return to_parasite2_.read();
|
||||
case 4: return to_parasite3_.data_available() | to_host3_.not_full();
|
||||
case 5: return to_parasite3_.read();
|
||||
case 6: return to_parasite4_.data_available() | to_host4_.not_full();
|
||||
case 7: return to_parasite4_.read();
|
||||
|
||||
default: __builtin_unreachable();
|
||||
}
|
||||
}
|
||||
|
||||
void host_write(const uint16_t address, const uint8_t value) {
|
||||
switch(address & 7) {
|
||||
case 0: set_status(value); break;
|
||||
case 1: to_parasite1_.write(value); break;
|
||||
case 3: to_parasite2_.write(value); break;
|
||||
case 5: to_parasite3_.write(value); break;
|
||||
case 7: to_parasite4_.write(value); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t host_read(const uint16_t address) {
|
||||
switch(address & 7) {
|
||||
case 0: return to_host1_.data_available() | to_parasite1_.not_full() | status();
|
||||
case 1: return to_host1_.read();
|
||||
case 2: return to_host2_.data_available() | to_parasite2_.not_full();
|
||||
case 3: return to_host2_.read();
|
||||
case 4: return to_host3_.data_available() | to_parasite3_.not_full();
|
||||
case 5: return to_host3_.read();
|
||||
case 6: return to_host4_.data_available() | to_parasite4_.not_full();
|
||||
case 7: return to_host4_.read();
|
||||
|
||||
default: __builtin_unreachable();
|
||||
}
|
||||
}
|
||||
|
||||
void set_reset(const bool reset) {
|
||||
if(reset_ == reset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a software approximtion of holding the reset state for as long
|
||||
// as it is signalled.
|
||||
if(!reset) {
|
||||
flags_ = 0x01;
|
||||
to_parasite1_.reset();
|
||||
to_parasite2_.reset();
|
||||
to_parasite3_.reset();
|
||||
to_parasite4_.reset();
|
||||
to_host1_.reset();
|
||||
to_host2_.reset();
|
||||
to_host3_.reset();
|
||||
to_host4_.reset();
|
||||
}
|
||||
reset_ = reset;
|
||||
|
||||
update_parasite_reset();
|
||||
}
|
||||
|
||||
private:
|
||||
void signal_changes(const uint8_t changes) {
|
||||
if(changes & 0x01) {
|
||||
host_.set_host_tube_irq(interrupt_sources_ & 0x01);
|
||||
}
|
||||
if(changes & 0x06) {
|
||||
host_.set_parasite_tube_irq(interrupt_sources_ & 0x06);
|
||||
}
|
||||
if(changes & 0x08) {
|
||||
host_.set_parasite_tube_nmi(interrupt_sources_ & 0x08);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t signalling_fifos() const {
|
||||
return interrupt_sources_ & flags_;
|
||||
}
|
||||
|
||||
void apply_fifo_mask(const uint8_t or_, const uint8_t and_) {
|
||||
const auto signalling = signalling_fifos();
|
||||
interrupt_sources_ = (interrupt_sources_ | or_) & and_;
|
||||
signal_changes(signalling_fifos() ^ signalling);
|
||||
}
|
||||
|
||||
void update_parasite_reset() {
|
||||
host_.set_parasite_reset((flags_ & 0x20) || reset_);
|
||||
}
|
||||
|
||||
uint8_t status() const {
|
||||
return flags_;
|
||||
}
|
||||
|
||||
void set_status(const uint8_t value) {
|
||||
const auto signalling = signalling_fifos();
|
||||
const uint8_t bits = value & 0x3f;
|
||||
if(value & 0x80) {
|
||||
flags_ |= bits;
|
||||
} else {
|
||||
flags_ &= ~bits;
|
||||
}
|
||||
signal_changes(signalling_fifos() ^ signalling);
|
||||
if(value & 0x20) {
|
||||
update_parasite_reset();
|
||||
}
|
||||
|
||||
// TODO: understand meaning of bits 4 and 6.
|
||||
}
|
||||
|
||||
HostT &host_;
|
||||
uint8_t flags_ = 0x01;
|
||||
bool reset_ = false;
|
||||
uint8_t interrupt_sources_ = 0x00;
|
||||
|
||||
FIFO<1, ULA> to_parasite1_;
|
||||
FIFO<1, ULA> to_parasite2_;
|
||||
FIFO<2, ULA> to_parasite3_;
|
||||
FIFO<1, ULA> to_parasite4_;
|
||||
|
||||
FIFO<24, ULA> to_host1_;
|
||||
FIFO<1, ULA> to_host2_;
|
||||
FIFO<2, ULA> to_host3_;
|
||||
FIFO<1, ULA> to_host4_;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -46,7 +46,7 @@ Chipset::Chipset(MemoryMap &map, int input_clock_rate) :
|
||||
|
||||
// Very conservatively crop, to roughly the centre 88% of a frame.
|
||||
// This rectange was specifically calibrated around the default Workbench display.
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.055f, 0.88f, 0.88f));
|
||||
crt_.set_fixed_framing(Outputs::Display::Rect(0.05f, 0.055f, 0.88f, 0.88f));
|
||||
}
|
||||
|
||||
#undef DMA_CONSTRUCT
|
||||
|
||||
@@ -127,7 +127,7 @@ public:
|
||||
}
|
||||
|
||||
~AYDeferrer() {
|
||||
audio_queue_.flush();
|
||||
audio_queue_.lock_flush();
|
||||
}
|
||||
|
||||
/// Adds @c half_cycles half cycles to the amount of time that has passed.
|
||||
@@ -175,11 +175,25 @@ public:
|
||||
interrupt_timer_(interrupt_timer) {
|
||||
establish_palette_hits();
|
||||
build_mode_table();
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f));
|
||||
crt_.set_brightness(3.0f / 2.0f); // As only the values 0, 1 and 2 will be used in each channel,
|
||||
// whereas Red2Green2Blue2 defines a range of 0-3.
|
||||
}
|
||||
|
||||
void set_dynamic_framing(const bool enable) {
|
||||
dynamic_framing_ = enable;
|
||||
if(enable) {
|
||||
crt_.set_dynamic_framing(
|
||||
Outputs::Display::Rect(0.16842f, 0.19909f, 0.71579f, 0.67197f),
|
||||
0.0f, 0.1f);
|
||||
} else {
|
||||
crt_.set_fixed_framing(Outputs::Display::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f));
|
||||
}
|
||||
}
|
||||
|
||||
bool dynamic_framing() const {
|
||||
return dynamic_framing_;
|
||||
}
|
||||
|
||||
/*!
|
||||
The CRTC entry function for the main part of each clock cycle; takes the current
|
||||
bus state and determines what output to produce based on the current palette and mode.
|
||||
@@ -377,14 +391,7 @@ private:
|
||||
|
||||
void output_border(const int length) {
|
||||
assert(length >= 0);
|
||||
|
||||
// A black border can be output via crt_.output_blank for a minor performance
|
||||
// win; otherwise paint whatever the border colour really is.
|
||||
if(border_) {
|
||||
crt_.output_level<uint8_t>(length * 16, border_);
|
||||
} else {
|
||||
crt_.output_blank(length * 16);
|
||||
}
|
||||
crt_.output_level<uint8_t>(length * 16, border_);
|
||||
}
|
||||
|
||||
template <typename IntT>
|
||||
@@ -548,6 +555,7 @@ private:
|
||||
int cycles_into_hsync_ = 0;
|
||||
|
||||
Outputs::CRT::CRT crt_;
|
||||
bool dynamic_framing_ = false;
|
||||
uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr;
|
||||
|
||||
const uint8_t *const ram_ = nullptr;
|
||||
@@ -1196,15 +1204,17 @@ public:
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->output = get_video_signal_configurable();
|
||||
options->quickload = allow_fast_tape_hack_;
|
||||
options->quick_load = allow_fast_tape_hack_;
|
||||
options->dynamic_crop = crtc_bus_handler_.dynamic_framing();
|
||||
return options;
|
||||
}
|
||||
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) {
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
set_video_signal_configurable(options->output);
|
||||
allow_fast_tape_hack_ = options->quickload;
|
||||
allow_fast_tape_hack_ = options->quick_load;
|
||||
set_use_fast_tape_hack();
|
||||
crtc_bus_handler_.set_dynamic_framing(options->dynamic_crop);
|
||||
}
|
||||
|
||||
// MARK: - Joysticks
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "Configurable/Configurable.hpp"
|
||||
#include "Configurable/StandardOptions.hpp"
|
||||
#include "Analyser/Static/StaticAnalyser.hpp"
|
||||
#include "Machines/ROMMachine.hpp"
|
||||
|
||||
#include <memory>
|
||||
@@ -32,28 +32,33 @@ struct Machine {
|
||||
/// Defines the runtime options available for an Amstrad CPC.
|
||||
class Options:
|
||||
public Reflection::StructImpl<Options>,
|
||||
public Configurable::DisplayOption<Options>,
|
||||
public Configurable::QuickloadOption<Options>
|
||||
public Configurable::Options::Display<Options>,
|
||||
public Configurable::Options::QuickLoad<Options>,
|
||||
public Configurable::Options::DynamicCrop<Options>
|
||||
{
|
||||
public:
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(Configurable::Display::RGB),
|
||||
Configurable::QuickloadOption<Options>(type == Configurable::OptionsType::UserFriendly) {}
|
||||
Configurable::Options::Display<Options>(Configurable::Display::RGB),
|
||||
Configurable::Options::QuickLoad<Options>(type == Configurable::OptionsType::UserFriendly),
|
||||
Configurable::Options::DynamicCrop<Options>(type == Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
private:
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
friend Configurable::QuickloadOption<Options>;
|
||||
friend Configurable::Options::Display<Options>;
|
||||
friend Configurable::Options::QuickLoad<Options>;
|
||||
friend Configurable::Options::DynamicCrop<Options>;
|
||||
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
declare_display_option();
|
||||
declare_quickload_option();
|
||||
declare_dynamic_crop_option();
|
||||
limit_enum(&output, Configurable::Display::RGB, Configurable::Display::CompositeColour, -1);
|
||||
}
|
||||
};
|
||||
|
||||
// Provided for running the SHAKER test suite.
|
||||
struct SSMDelegate {
|
||||
virtual void perform(uint16_t) = 0;
|
||||
};
|
||||
|
||||
@@ -678,7 +678,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
audio_queue_.lock_flush();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
|
||||
@@ -24,15 +24,15 @@ struct Machine {
|
||||
static std::unique_ptr<Machine> AppleII(const Analyser::Static::Target *, const ROMMachine::ROMFetcher &);
|
||||
|
||||
/// Defines the runtime options available for an Apple II.
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::Options::Display<Options> {
|
||||
friend Configurable::Options::Display<Options>;
|
||||
public:
|
||||
bool use_square_pixels = false;
|
||||
|
||||
Options(Configurable::OptionsType) :
|
||||
Configurable::DisplayOption<Options>(Configurable::Display::CompositeColour) {}
|
||||
Options(const Configurable::OptionsType) :
|
||||
Configurable::Options::Display<Options>(Configurable::Display::CompositeColour) {}
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
|
||||
@@ -38,9 +38,13 @@ public:
|
||||
Input(Input::Fire, 2),
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &input, float value) final {
|
||||
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical))
|
||||
void did_set_input(const Input &input, const float value) final {
|
||||
if(
|
||||
!input.info.control.index &&
|
||||
(input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical)
|
||||
) {
|
||||
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
|
||||
}
|
||||
}
|
||||
|
||||
void did_set_input(const Input &input, bool value) final {
|
||||
|
||||
@@ -25,20 +25,8 @@ 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) {
|
||||
void VideoBase::set_use_square_pixels(const 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
|
||||
@@ -55,6 +43,13 @@ void VideoBase::set_use_square_pixels(bool use_square_pixels) {
|
||||
crt_.set_aspect_ratio(4.0f / 3.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBase::establish_framing() {
|
||||
crt_.set_fixed_framing([&] {
|
||||
run_for(Cycles(10'000));
|
||||
});
|
||||
}
|
||||
|
||||
bool VideoBase::get_use_square_pixels() const {
|
||||
return use_square_pixels_;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ class VideoBase: public VideoSwitches<Cycles> {
|
||||
public:
|
||||
VideoBase(bool is_iie, std::function<void(Cycles)> &&target);
|
||||
|
||||
void establish_framing();
|
||||
|
||||
/// Sets the scan target.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
@@ -119,8 +121,11 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
public:
|
||||
/// Constructs an instance of the video feed; a CRT is also created.
|
||||
Video(BusHandler &bus_handler) :
|
||||
VideoBase(is_iie, [this] (Cycles cycles) { advance(cycles); }),
|
||||
bus_handler_(bus_handler) {}
|
||||
VideoBase(is_iie, [this] (const Cycles cycles) { advance(cycles); }),
|
||||
bus_handler_(bus_handler)
|
||||
{
|
||||
establish_framing();
|
||||
}
|
||||
|
||||
/*!
|
||||
Obtains the last value the video read prior to time now+offset, according to the *current*
|
||||
|
||||
@@ -210,7 +210,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
audio_queue_.lock_flush();
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) override {
|
||||
|
||||
@@ -111,11 +111,6 @@ Video::Video() :
|
||||
VideoSwitches<Cycles>(true, Cycles(2), [this] (Cycles cycles) { advance(cycles); }),
|
||||
crt_(CyclesPerLine - 1, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red4Green4Blue4) {
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.097f, 0.1f, 0.85f, 0.85f));
|
||||
|
||||
// Reduce the initial bounce by cueing up the part of the frame that initial drawing actually
|
||||
// starts with. More or less.
|
||||
crt_.output_blank(228*63*2);
|
||||
|
||||
// Establish the shift lookup table for NTSC -> RGB output.
|
||||
for(size_t c = 0; c < sizeof(ntsc_delay_lookup_) / sizeof(*ntsc_delay_lookup_); c++) {
|
||||
@@ -149,8 +144,11 @@ Outputs::Display::DisplayType Video::get_display_type() const {
|
||||
return crt_.get_display_type();
|
||||
}
|
||||
|
||||
void Video::set_internal_ram(const uint8_t *ram) {
|
||||
void Video::set_internal_ram(const uint8_t *const ram) {
|
||||
ram_ = ram;
|
||||
// crt_.set_automatic_fixed_framing([&] {
|
||||
// run_for(Cycles(10'000));
|
||||
// });
|
||||
}
|
||||
|
||||
void Video::advance(Cycles cycles) {
|
||||
@@ -410,15 +408,7 @@ void Video::output_row(int row, int start, int end) {
|
||||
// Output right border as far as currently known.
|
||||
if(start >= start_of_right_border && start < start_of_sync) {
|
||||
const int end_of_period = std::min(start_of_sync, end);
|
||||
|
||||
if(border_colour_) {
|
||||
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
|
||||
if(pixel) *pixel = border_colour_;
|
||||
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
|
||||
} else {
|
||||
crt_.output_blank((end_of_period - start) * CyclesPerTick);
|
||||
}
|
||||
|
||||
crt_.output_level<uint16_t>((end_of_period - start) * CyclesPerTick, border_colour_);
|
||||
// There's no point updating start here; just fall
|
||||
// through to the end == FinalColumn test.
|
||||
}
|
||||
@@ -426,15 +416,7 @@ void Video::output_row(int row, int start, int end) {
|
||||
// This line is all border, all the time.
|
||||
if(start >= start_of_left_border && start < start_of_sync) {
|
||||
const int end_of_period = std::min(start_of_sync, end);
|
||||
|
||||
if(border_colour_) {
|
||||
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
|
||||
if(pixel) *pixel = border_colour_;
|
||||
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
|
||||
} else {
|
||||
crt_.output_blank((end_of_period - start) * CyclesPerTick);
|
||||
}
|
||||
|
||||
crt_.output_level<uint16_t>((end_of_period - start) * CyclesPerTick, border_colour_);
|
||||
start = end_of_period;
|
||||
if(start == end) return;
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_.queue.flush();
|
||||
audio_.queue.lock_flush();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
@@ -506,7 +506,7 @@ public:
|
||||
// MARK: - Configuration options.
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->quickboot = quickboot_;
|
||||
options->quick_boot = quickboot_;
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -515,7 +515,7 @@ public:
|
||||
// It should probably be a construction option.
|
||||
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
quickboot_ = options->quickboot;
|
||||
quickboot_ = options->quick_boot;
|
||||
|
||||
using Model = Analyser::Static::Macintosh::Target::Model;
|
||||
const bool is_plus_rom = model == Model::Mac512ke || model == Model::MacPlus;
|
||||
|
||||
@@ -21,11 +21,11 @@ struct Machine {
|
||||
/// Creates and returns a Macintosh.
|
||||
static std::unique_ptr<Machine> Macintosh(const Analyser::Static::Target *, const ROMMachine::ROMFetcher &);
|
||||
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::QuickbootOption<Options> {
|
||||
friend Configurable::QuickbootOption<Options>;
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::Options::QuickBoot<Options> {
|
||||
friend Configurable::Options::QuickBoot<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::QuickbootOption<Options>(type == Configurable::OptionsType::UserFriendly) {}
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::Options::QuickBoot<Options>(type == Configurable::OptionsType::UserFriendly) {}
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
|
||||
@@ -39,17 +39,6 @@ Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulato
|
||||
crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) {
|
||||
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
|
||||
// UGLY HACK. UGLY, UGLY HACK. UGLY!
|
||||
// The OpenGL scan target fails properly to place visible areas which are not 4:3.
|
||||
// The [newer] Metal scan target has no such issue. So assume that Apple => Metal,
|
||||
// and set a visible area to work around the OpenGL issue if required.
|
||||
// TODO: eliminate UGLY HACK.
|
||||
#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));
|
||||
#endif
|
||||
crt_.set_aspect_ratio(1.73f); // The Mac uses a non-standard scanning area.
|
||||
}
|
||||
|
||||
@@ -191,7 +180,12 @@ void Video::set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use
|
||||
use_alternate_audio_buffer_ = use_alternate_audio_buffer;
|
||||
}
|
||||
|
||||
void Video::set_ram(uint16_t *ram, uint32_t mask) {
|
||||
void Video::set_ram(const uint16_t *const ram, const uint32_t mask) {
|
||||
ram_ = ram;
|
||||
ram_mask_ = mask;
|
||||
|
||||
// Now that RAM is assigned, the CRT cna be warmed.
|
||||
crt_.set_fixed_framing([&] {
|
||||
run_for(Cycles(10'000));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public:
|
||||
Provides a base address and a mask indicating which parts of the generated video and audio/drive addresses are
|
||||
actually decoded, accessing *word-sized memory*; e.g. for a 128kb Macintosh this should be (1 << 16) - 1 = 0xffff.
|
||||
*/
|
||||
void set_ram(uint16_t *ram, uint32_t mask);
|
||||
void set_ram(const uint16_t *ram, uint32_t mask);
|
||||
|
||||
/*!
|
||||
@returns @c true if the video is currently outputting a vertical sync, @c false otherwise.
|
||||
@@ -86,7 +86,7 @@ private:
|
||||
DriveSpeedAccumulator &drive_speed_accumulator_;
|
||||
|
||||
Outputs::CRT::CRT crt_;
|
||||
uint16_t *ram_ = nullptr;
|
||||
const uint16_t *ram_ = nullptr;
|
||||
uint32_t ram_mask_ = 0;
|
||||
|
||||
HalfCycles frame_position_;
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
#include "Machines/MachineTypes.hpp"
|
||||
|
||||
#include "Outputs/CRT/MismatchWarner.hpp"
|
||||
|
||||
#include "Analyser/Static/Atari2600/Target.hpp"
|
||||
|
||||
#include "Cartridges/Atari8k.hpp"
|
||||
@@ -157,7 +159,7 @@ public:
|
||||
|
||||
// to satisfy CRTMachine::Machine
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
bus_->speaker_.set_input_rate(float(get_clock_rate() / double(CPUTicksPerAudioTick)));
|
||||
bus_->audio_.speaker().set_input_rate(float(get_clock_rate() / double(CPUTicksPerAudioTick)));
|
||||
bus_->tia_.set_crt_delegate(&frequency_mismatch_warner_);
|
||||
bus_->tia_.set_scan_target(scan_target);
|
||||
}
|
||||
@@ -167,7 +169,7 @@ public:
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &bus_->speaker_;
|
||||
return &bus_->audio_.speaker();
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) final {
|
||||
@@ -204,11 +206,11 @@ private:
|
||||
// a confidence counter
|
||||
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
|
||||
|
||||
void set_is_ntsc(bool is_ntsc) {
|
||||
void set_is_ntsc(const bool is_ntsc) {
|
||||
bus_->tia_.set_output_mode(is_ntsc ? TIA::OutputMode::NTSC : TIA::OutputMode::PAL);
|
||||
const double clock_rate = is_ntsc ? NTSC_clock_rate : PAL_clock_rate;
|
||||
bus_->speaker_.set_input_rate(float(clock_rate) / float(CPUTicksPerAudioTick));
|
||||
bus_->speaker_.set_high_frequency_cutoff(float(clock_rate) / float(CPUTicksPerAudioTick * 2));
|
||||
bus_->audio_.speaker().set_input_rate(float(clock_rate) / float(CPUTicksPerAudioTick));
|
||||
bus_->audio_.speaker().set_high_frequency_cutoff(float(clock_rate) / float(CPUTicksPerAudioTick * 2));
|
||||
set_clock_rate(clock_rate);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,12 +21,10 @@ namespace Atari2600 {
|
||||
|
||||
class Bus {
|
||||
public:
|
||||
Bus() :
|
||||
tia_sound_(audio_queue_),
|
||||
speaker_(tia_sound_) {}
|
||||
Bus() : audio_(Cycles(CPUTicksPerAudioTick * 3)) {}
|
||||
|
||||
virtual ~Bus() {
|
||||
audio_queue_.flush();
|
||||
audio_.stop();
|
||||
}
|
||||
|
||||
virtual void run_for(const Cycles cycles) = 0;
|
||||
@@ -34,31 +32,22 @@ public:
|
||||
virtual void set_reset_line(bool state) = 0;
|
||||
virtual void flush() = 0;
|
||||
|
||||
// the RIOT, TIA and speaker
|
||||
// The RIOT, TIA and speaker.
|
||||
PIA mos6532_;
|
||||
TIA tia_;
|
||||
Outputs::Speaker::PullLowpassSpeakerQueue<Cycles, TIASound> audio_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
TIASound tia_sound_;
|
||||
Outputs::Speaker::PullLowpass<TIASound> speaker_;
|
||||
|
||||
// joystick state
|
||||
// Joystick state.
|
||||
uint8_t tia_input_value_[2] = {0xff, 0xff};
|
||||
|
||||
protected:
|
||||
// speaker backlog accumlation counter
|
||||
Cycles cycles_since_speaker_update_;
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_speaker_update_.divide(Cycles(CPUTicksPerAudioTick * 3)));
|
||||
}
|
||||
|
||||
// video backlog accumulation counter
|
||||
// Video backlog accumulation counter.
|
||||
Cycles cycles_since_video_update_;
|
||||
inline void update_video() {
|
||||
tia_.run_for(cycles_since_video_update_.flush<Cycles>());
|
||||
}
|
||||
|
||||
// RIOT backlog accumulation counter
|
||||
// RIOT backlog accumulation counter.
|
||||
Cycles cycles_since_6532_update_;
|
||||
inline void update_6532() {
|
||||
mos6532_.run_for(cycles_since_6532_update_.flush<Cycles>());
|
||||
|
||||
@@ -70,10 +70,11 @@ public:
|
||||
// leap to the end of ready only once ready is signalled because on a 6502 ready doesn't take
|
||||
// effect until the next read; therefore it isn't safe to assume that signalling ready immediately
|
||||
// skips to the end of the line.
|
||||
if(operation == CPU::MOS6502::BusOperation::Ready)
|
||||
if(operation == CPU::MOS6502::BusOperation::Ready) {
|
||||
cycles_run_for = tia_.get_cycles_until_horizontal_blank(cycles_since_video_update_);
|
||||
}
|
||||
|
||||
cycles_since_speaker_update_ += Cycles(cycles_run_for);
|
||||
audio_ += Cycles(cycles_run_for);
|
||||
cycles_since_video_update_ += Cycles(cycles_run_for);
|
||||
cycles_since_6532_update_ += Cycles(cycles_run_for / 3);
|
||||
bus_extender_.advance_cycles(cycles_run_for / 3);
|
||||
@@ -171,11 +172,11 @@ public:
|
||||
case 0x2c: update_video(); tia_.clear_collision_flags(); break;
|
||||
|
||||
case 0x15:
|
||||
case 0x16: update_audio(); tia_sound_.set_control(decodedAddress - 0x15, *value); break;
|
||||
case 0x16: audio_->set_control(decodedAddress - 0x15, *value); break;
|
||||
case 0x17:
|
||||
case 0x18: update_audio(); tia_sound_.set_divider(decodedAddress - 0x17, *value); break;
|
||||
case 0x18: audio_->set_divider(decodedAddress - 0x17, *value); break;
|
||||
case 0x19:
|
||||
case 0x1a: update_audio(); tia_sound_.set_volume(decodedAddress - 0x19, *value); break;
|
||||
case 0x1a: audio_->set_volume(decodedAddress - 0x19, *value); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,9 +202,8 @@ public:
|
||||
}
|
||||
|
||||
void flush() override {
|
||||
update_audio();
|
||||
update_video();
|
||||
audio_queue_.perform();
|
||||
audio_.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
using namespace Atari2600;
|
||||
|
||||
Atari2600::TIASound::TIASound(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
Atari2600::TIASound::TIASound(Outputs::Speaker::TaskQueue &audio_queue) :
|
||||
audio_queue_(audio_queue)
|
||||
{}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
#include "Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "Outputs/Speaker/SpeakerQueue.hpp"
|
||||
|
||||
namespace Atari2600 {
|
||||
|
||||
@@ -19,7 +19,7 @@ constexpr int CPUTicksPerAudioTick = 2;
|
||||
|
||||
class TIASound: public Outputs::Speaker::BufferSource<TIASound, false> {
|
||||
public:
|
||||
TIASound(Concurrency::AsyncTaskQueue<false> &);
|
||||
TIASound(Outputs::Speaker::TaskQueue &);
|
||||
|
||||
void set_volume(int channel, uint8_t volume);
|
||||
void set_divider(int channel, uint8_t divider);
|
||||
@@ -30,7 +30,7 @@ public:
|
||||
void set_sample_volume_range(std::int16_t);
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Outputs::Speaker::TaskQueue &audio_queue_;
|
||||
|
||||
uint8_t volume_[2];
|
||||
uint8_t divider_[2];
|
||||
|
||||
@@ -144,7 +144,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
audio_queue_.lock_flush();
|
||||
}
|
||||
|
||||
// MARK: CRTMachine::Machine
|
||||
|
||||
@@ -22,15 +22,15 @@ struct Machine {
|
||||
|
||||
static std::unique_ptr<Machine> AtariST(const Analyser::Static::Target *, const ROMMachine::ROMFetcher &);
|
||||
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::Options::Display<Options> {
|
||||
friend Configurable::Options::Display<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) : Configurable::DisplayOption<Options>(
|
||||
Options(const Configurable::OptionsType type) : Configurable::Options::Display<Options>(
|
||||
type == Configurable::OptionsType::UserFriendly ?
|
||||
Configurable::Display::RGB : Configurable::Display::CompositeColour) {}
|
||||
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
|
||||
@@ -127,10 +127,9 @@ Video::Video() :
|
||||
|
||||
// Show a total of 260 lines; a little short for PAL but a compromise between that and the ST's
|
||||
// usual output height of 200 lines.
|
||||
crt_.set_visible_area(crt_.get_rect_for_area(
|
||||
crt_.set_fixed_framing(crt_.get_rect_for_area(
|
||||
33, 260,
|
||||
480, 1280,
|
||||
4.0f / 3.0f));
|
||||
480, 1280));
|
||||
}
|
||||
|
||||
void Video::set_ram(uint16_t *ram, size_t size) {
|
||||
|
||||
@@ -168,7 +168,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
audio_queue_.lock_flush();
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
|
||||
@@ -21,15 +21,15 @@ struct Machine {
|
||||
virtual ~Machine() = default;
|
||||
static std::unique_ptr<Machine> ColecoVision(const Analyser::Static::Target *, const ROMMachine::ROMFetcher &);
|
||||
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::Options::Display<Options> {
|
||||
friend Configurable::Options::Display<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::Options::Display<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Configurable::Display::SVideo : Configurable::Display::CompositeColour) {}
|
||||
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
using namespace Commodore::C1540;
|
||||
|
||||
namespace {
|
||||
ROM::Name rom_name(Personality personality) {
|
||||
|
||||
// MARK: - Construction, including ROM requests.
|
||||
|
||||
ROM::Name rom_name(const Personality personality) {
|
||||
switch(personality) {
|
||||
default:
|
||||
case Personality::C1540: return ROM::Name::Commodore1540;
|
||||
@@ -26,11 +29,11 @@ ROM::Name rom_name(Personality personality) {
|
||||
}
|
||||
}
|
||||
|
||||
ROM::Request Machine::rom_request(Personality personality) {
|
||||
ROM::Request Machine::rom_request(const Personality personality) {
|
||||
return ROM::Request(rom_name(personality));
|
||||
}
|
||||
|
||||
MachineBase::MachineBase(Personality personality, const ROM::Map &roms) :
|
||||
MachineBase::MachineBase(const Personality personality, const ROM::Map &roms) :
|
||||
Storage::Disk::Controller(1000000),
|
||||
m6502_(*this),
|
||||
drive_VIA_(drive_VIA_port_handler_),
|
||||
@@ -58,14 +61,13 @@ MachineBase::MachineBase(Personality personality, const ROM::Map &roms) :
|
||||
std::memcpy(rom_, rom->second.data(), std::min(sizeof(rom_), rom->second.size()));
|
||||
}
|
||||
|
||||
Machine::Machine(Personality personality, const ROM::Map &roms) :
|
||||
Machine::Machine(const Personality personality, const ROM::Map &roms) :
|
||||
MachineBase(personality, roms) {}
|
||||
|
||||
void Machine::set_serial_bus(Commodore::Serial::Bus &serial_bus) {
|
||||
Commodore::Serial::attach(serial_port_, serial_bus);
|
||||
}
|
||||
// MARK: - 6502 bus.
|
||||
|
||||
Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
|
||||
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
|
||||
Cycles MachineBase::perform(const AddressT address, CPU::MOS6502Mk2::data_t<operation> value) {
|
||||
/*
|
||||
Memory map (given that I'm unsure yet on any potential mirroring):
|
||||
|
||||
@@ -75,24 +77,28 @@ Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation,
|
||||
0xc000-0xffff ROM
|
||||
*/
|
||||
if(address < 0x800) {
|
||||
if(is_read(operation))
|
||||
*value = ram_[address];
|
||||
if constexpr (is_read(operation))
|
||||
value = ram_[address];
|
||||
else
|
||||
ram_[address] = *value;
|
||||
ram_[address] = value;
|
||||
} else if(address >= 0xc000) {
|
||||
if(is_read(operation)) {
|
||||
*value = rom_[address & 0x3fff];
|
||||
if constexpr (is_read(operation)) {
|
||||
value = rom_[address & 0x3fff];
|
||||
}
|
||||
} else if(address >= 0x1800 && address <= 0x180f) {
|
||||
if(is_read(operation))
|
||||
*value = serial_port_VIA_.read(address);
|
||||
if constexpr (is_read(operation))
|
||||
value = serial_port_VIA_.read(address);
|
||||
else
|
||||
serial_port_VIA_.write(address, *value);
|
||||
serial_port_VIA_.write(address, value);
|
||||
} else if(address >= 0x1c00 && address <= 0x1c0f) {
|
||||
if(is_read(operation))
|
||||
*value = drive_VIA_.read(address);
|
||||
if constexpr (is_read(operation))
|
||||
value = drive_VIA_.read(address);
|
||||
else
|
||||
drive_VIA_.write(address, *value);
|
||||
drive_VIA_.write(address, value);
|
||||
} else {
|
||||
if constexpr (is_read(operation)) {
|
||||
value = 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
serial_port_VIA_.run_for(Cycles(1));
|
||||
@@ -101,34 +107,42 @@ Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation,
|
||||
return Cycles(1);
|
||||
}
|
||||
|
||||
void Machine::set_disk(std::shared_ptr<Storage::Disk::Disk> disk) {
|
||||
get_drive().set_disk(disk);
|
||||
}
|
||||
|
||||
void Machine::run_for(const Cycles cycles) {
|
||||
m6502_.run_for(cycles);
|
||||
|
||||
const bool drive_motor = drive_VIA_port_handler_.get_motor_enabled();
|
||||
const bool drive_motor = drive_VIA_port_handler_.motor_enabled();
|
||||
get_drive().set_motor_on(drive_motor);
|
||||
if(drive_motor)
|
||||
if(drive_motor) {
|
||||
Storage::Disk::Controller::run_for(cycles);
|
||||
}
|
||||
}
|
||||
|
||||
void MachineBase::set_activity_observer(Activity::Observer *observer) {
|
||||
// MARK: - External attachments.
|
||||
|
||||
void Machine::set_serial_bus(Commodore::Serial::Bus &serial_bus) {
|
||||
Commodore::Serial::attach(serial_port_, serial_bus);
|
||||
}
|
||||
|
||||
void Machine::set_disk(std::shared_ptr<Storage::Disk::Disk> disk) {
|
||||
get_drive().set_disk(disk);
|
||||
drive_VIA_port_handler_.set_is_read_only(disk->is_read_only());
|
||||
}
|
||||
|
||||
void MachineBase::set_activity_observer(Activity::Observer *const observer) {
|
||||
drive_VIA_.bus_handler().set_activity_observer(observer);
|
||||
get_drive().set_activity_observer(observer, "Drive", false);
|
||||
}
|
||||
|
||||
// MARK: - 6522 delegate
|
||||
// MARK: - 6522 delegate.
|
||||
|
||||
void MachineBase::mos6522_did_change_interrupt_status(void *) {
|
||||
// both VIAs are connected to the IRQ line
|
||||
m6502_.set_irq_line(serial_port_VIA_.get_interrupt_line() || drive_VIA_.get_interrupt_line());
|
||||
m6502_.set<CPU::MOS6502Mk2::Line::IRQ>(serial_port_VIA_.get_interrupt_line() || drive_VIA_.get_interrupt_line());
|
||||
}
|
||||
|
||||
// MARK: - Disk drive
|
||||
// MARK: - Disk drive.
|
||||
|
||||
void MachineBase::process_input_bit(int value) {
|
||||
void MachineBase::process_input_bit(const int value) {
|
||||
shift_register_ = (shift_register_ << 1) | value;
|
||||
if((shift_register_ & 0x3ff) == 0x3ff) {
|
||||
drive_VIA_port_handler_.set_sync_detected(true);
|
||||
@@ -140,11 +154,11 @@ void MachineBase::process_input_bit(int value) {
|
||||
if(bit_window_offset_ == 8) {
|
||||
drive_VIA_port_handler_.set_data_input(uint8_t(shift_register_));
|
||||
bit_window_offset_ = 0;
|
||||
if(drive_VIA_port_handler_.get_should_set_overflow()) {
|
||||
m6502_.set_overflow_line(true);
|
||||
if(drive_VIA_port_handler_.should_set_overflow()) {
|
||||
m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(true);
|
||||
}
|
||||
}
|
||||
else m6502_.set_overflow_line(false);
|
||||
else m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(false);
|
||||
}
|
||||
|
||||
// the 1540 does not recognise index holes
|
||||
@@ -164,7 +178,9 @@ void MachineBase::drive_via_did_set_data_density(DriveVIA &, const int density)
|
||||
|
||||
template <MOS::MOS6522::Port port>
|
||||
uint8_t SerialPortVIA::get_port_input() const {
|
||||
if(port) return port_b_;
|
||||
if(port) {
|
||||
return port_b_;
|
||||
}
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
@@ -203,13 +219,15 @@ void SerialPortVIA::set_serial_port(Commodore::Serial::Port &port) {
|
||||
|
||||
void SerialPortVIA::update_data_line() {
|
||||
// "ATN (Attention) is an input on pin 3 of P2 and P3 that is sensed at PB7 and CA1 of UC3 after being inverted by UA1"
|
||||
serial_port_->set_output(::Commodore::Serial::Line::Data,
|
||||
Serial::LineLevel(!data_level_output_ && (attention_level_input_ != attention_acknowledge_level_)));
|
||||
serial_port_->set_output(
|
||||
::Commodore::Serial::Line::Data,
|
||||
Serial::LineLevel(!data_level_output_ && (attention_level_input_ != attention_acknowledge_level_))
|
||||
);
|
||||
}
|
||||
|
||||
// MARK: - DriveVIA
|
||||
|
||||
void DriveVIA::set_delegate(Delegate *delegate) {
|
||||
void DriveVIA::set_delegate(Delegate *const delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
|
||||
@@ -220,18 +238,22 @@ uint8_t DriveVIA::get_port_input() const {
|
||||
}
|
||||
|
||||
void DriveVIA::set_sync_detected(const bool sync_detected) {
|
||||
port_b_ = (port_b_ & 0x7f) | (sync_detected ? 0x00 : 0x80);
|
||||
port_b_ = (port_b_ & ~0x80) | (sync_detected ? 0x00 : 0x80);
|
||||
}
|
||||
|
||||
void DriveVIA::set_data_input(uint8_t value) {
|
||||
void DriveVIA::set_is_read_only(const bool is_read_only) {
|
||||
port_b_ = (port_b_ & ~0x10) | (is_read_only ? 0x00 : 0x10);
|
||||
}
|
||||
|
||||
void DriveVIA::set_data_input(const uint8_t value) {
|
||||
port_a_ = value;
|
||||
}
|
||||
|
||||
bool DriveVIA::get_should_set_overflow() {
|
||||
bool DriveVIA::should_set_overflow() {
|
||||
return should_set_overflow_;
|
||||
}
|
||||
|
||||
bool DriveVIA::get_motor_enabled() {
|
||||
bool DriveVIA::motor_enabled() {
|
||||
return drive_motor_;
|
||||
}
|
||||
|
||||
@@ -240,36 +262,48 @@ void DriveVIA::set_control_line_output(const bool value) {
|
||||
if(port == MOS::MOS6522::Port::A && line == MOS::MOS6522::Line::Two) {
|
||||
should_set_overflow_ = value;
|
||||
}
|
||||
}
|
||||
|
||||
template <MOS::MOS6522::Port port>
|
||||
void DriveVIA::set_port_output(const uint8_t value, uint8_t) {
|
||||
if(port) {
|
||||
if(previous_port_b_output_ != value) {
|
||||
// Record drive motor state.
|
||||
drive_motor_ = value&4;
|
||||
|
||||
// Check for a head step.
|
||||
const int step_difference = ((value&3) - (previous_port_b_output_&3))&3;
|
||||
if(step_difference) {
|
||||
if(delegate_) delegate_->drive_via_did_step_head(*this, (step_difference == 1) ? 1 : -1);
|
||||
}
|
||||
|
||||
// Check for a change in density.
|
||||
const int density_difference = (previous_port_b_output_^value) & (3 << 5);
|
||||
if(density_difference && delegate_) {
|
||||
delegate_->drive_via_did_set_data_density(*this, (value >> 5)&3);
|
||||
}
|
||||
|
||||
// Post the LED status.
|
||||
if(observer_) observer_->set_led_status("Drive", value&8);
|
||||
|
||||
previous_port_b_output_ = value;
|
||||
if(port == MOS::MOS6522::Port::B && line == MOS::MOS6522::Line::Two) {
|
||||
// TODO: 0 = write, 1 = read.
|
||||
if(!value) {
|
||||
printf("NOT IMPLEMENTED: write mode\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DriveVIA::set_activity_observer(Activity::Observer *observer) {
|
||||
template <>
|
||||
void DriveVIA::set_port_output<MOS::MOS6522::Port::B>(const uint8_t value, uint8_t) {
|
||||
if(previous_port_b_output_ != value) {
|
||||
// Record drive motor state.
|
||||
drive_motor_ = value&4;
|
||||
|
||||
// Check for a head step.
|
||||
const int step_difference = ((value&3) - (previous_port_b_output_&3))&3;
|
||||
if(step_difference && delegate_) {
|
||||
delegate_->drive_via_did_step_head(*this, (step_difference == 1) ? 1 : -1);
|
||||
}
|
||||
|
||||
// Check for a change in density.
|
||||
const int density_difference = (previous_port_b_output_^value) & (3 << 5);
|
||||
if(density_difference && delegate_) {
|
||||
delegate_->drive_via_did_set_data_density(*this, (value >> 5)&3);
|
||||
}
|
||||
|
||||
// Post the LED status.
|
||||
if(observer_) {
|
||||
observer_->set_led_status("Drive", value&8);
|
||||
}
|
||||
|
||||
previous_port_b_output_ = value;
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
void DriveVIA::set_port_output<MOS::MOS6522::Port::A>(const uint8_t value, uint8_t) {
|
||||
printf("TODO: output is %02x\n", value);
|
||||
}
|
||||
|
||||
void DriveVIA::set_activity_observer(Activity::Observer *const observer) {
|
||||
observer_ = observer;
|
||||
if(observer) {
|
||||
observer->register_led("Drive");
|
||||
@@ -277,9 +311,9 @@ void DriveVIA::set_activity_observer(Activity::Observer *observer) {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SerialPort
|
||||
// MARK: - SerialPort.
|
||||
|
||||
void SerialPort::set_input(Serial::Line line, Serial::LineLevel level) {
|
||||
void SerialPort::set_input(const Serial::Line line, const Serial::LineLevel level) {
|
||||
serial_port_via_->set_serial_line_state(line, bool(level), *via_);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Processors/6502/6502.hpp"
|
||||
#include "Processors/6502Mk2/6502Mk2.hpp"
|
||||
#include "Components/6522/6522.hpp"
|
||||
|
||||
#include "Machines/Commodore/SerialBus.hpp"
|
||||
@@ -65,15 +65,17 @@ private:
|
||||
It is wired up such that Port B contains:
|
||||
Bits 0/1: head step direction
|
||||
Bit 2: motor control
|
||||
Bit 3: LED control (TODO)
|
||||
Bit 3: LED control
|
||||
Bit 4: write protect photocell status (TODO)
|
||||
Bits 5/6: read/write density
|
||||
Bit 7: 0 if sync marks are currently being detected, 1 otherwise.
|
||||
|
||||
... and Port A contains the byte most recently read from the disk or the byte next to write to the disk, depending on data direction.
|
||||
|
||||
It is implied that CA2 might be used to set processor overflow, CA1 a strobe for data input, and one of the CBs being definitive on
|
||||
whether the disk head is being told to read or write, but it's unclear and I've yet to investigate. So, TODO.
|
||||
Elsewhere:
|
||||
* CA2 might is used to set processor overflow;
|
||||
* CA1 a strobe for data input; and
|
||||
* CB2 indicates read/write mode; 1 = read, 0 = write.
|
||||
*/
|
||||
class DriveVIA: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
public:
|
||||
@@ -88,8 +90,9 @@ public:
|
||||
|
||||
void set_sync_detected(bool);
|
||||
void set_data_input(uint8_t);
|
||||
bool get_should_set_overflow();
|
||||
bool get_motor_enabled();
|
||||
void set_is_read_only(bool);
|
||||
bool should_set_overflow();
|
||||
bool motor_enabled();
|
||||
|
||||
template <MOS::MOS6522::Port, MOS::MOS6522::Line>
|
||||
void set_control_line_output(bool value);
|
||||
@@ -122,7 +125,6 @@ private:
|
||||
};
|
||||
|
||||
class MachineBase:
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
|
||||
public DriveVIA::Delegate,
|
||||
public Storage::Disk::Controller {
|
||||
@@ -134,7 +136,8 @@ public:
|
||||
void set_activity_observer(Activity::Observer *);
|
||||
|
||||
// to satisfy CPU::MOS6502::Processor
|
||||
Cycles perform_bus_operation(CPU::MOS6502::BusOperation, uint16_t address, uint8_t *value);
|
||||
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
|
||||
Cycles perform(const AddressT, CPU::MOS6502Mk2::data_t<operation>);
|
||||
|
||||
protected:
|
||||
// to satisfy MOS::MOS6522::Delegate
|
||||
@@ -144,7 +147,12 @@ protected:
|
||||
void drive_via_did_step_head(DriveVIA &, int direction) override;
|
||||
void drive_via_did_set_data_density(DriveVIA &, int density) override;
|
||||
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, MachineBase, false> m6502_;
|
||||
struct M6502Traits {
|
||||
static constexpr auto uses_ready_line = false;
|
||||
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::AnyCycle;
|
||||
using BusHandlerT = MachineBase;
|
||||
};
|
||||
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
|
||||
|
||||
uint8_t ram_[0x800];
|
||||
uint8_t rom_[0x4000];
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
#include "Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "Outputs/Speaker/SpeakerQueue.hpp"
|
||||
|
||||
namespace Commodore::Plus4 {
|
||||
|
||||
class Audio: public Outputs::Speaker::BufferSource<Audio, false> {
|
||||
public:
|
||||
Audio(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
Audio(Outputs::Speaker::TaskQueue &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
template <Outputs::Speaker::Action action>
|
||||
@@ -122,7 +122,7 @@ public:
|
||||
|
||||
private:
|
||||
// Calling-thread state.
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Outputs::Speaker::TaskQueue &audio_queue_;
|
||||
|
||||
// Audio-thread state.
|
||||
int16_t external_volume_ = 0;
|
||||
|
||||
+190
-214
@@ -16,7 +16,7 @@
|
||||
|
||||
#include "Machines/MachineTypes.hpp"
|
||||
#include "Machines/Utility/MemoryFuzzer.hpp"
|
||||
#include "Processors/6502/6502.hpp"
|
||||
#include "Processors/6502Mk2/6502Mk2.hpp"
|
||||
#include "Analyser/Static/Commodore/Target.hpp"
|
||||
#include "Outputs/Log.hpp"
|
||||
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
@@ -186,13 +186,11 @@ public:
|
||||
interrupts_(*this),
|
||||
timers_(interrupts_),
|
||||
video_(video_map_, interrupts_),
|
||||
audio_(audio_queue_),
|
||||
speaker_(audio_)
|
||||
audio_(clock_rate(false), Cycles(1))
|
||||
{
|
||||
const auto clock = clock_rate(false);
|
||||
media_divider_ = Cycles(clock);
|
||||
set_clock_rate(clock);
|
||||
speaker_.set_input_rate(float(clock));
|
||||
|
||||
const auto kernel = ROM::Name::Plus4KernelPALv5;
|
||||
const auto basic = ROM::Name::Plus4BASIC;
|
||||
@@ -236,20 +234,17 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
audio_.stop();
|
||||
}
|
||||
|
||||
// HACK. NOCOMMIT.
|
||||
// int pulse_num_ = 0;
|
||||
|
||||
Cycles perform_bus_operation(
|
||||
const CPU::MOS6502::BusOperation operation,
|
||||
const uint16_t address,
|
||||
uint8_t *const value
|
||||
) {
|
||||
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
|
||||
Cycles perform(const AddressT address, CPU::MOS6502Mk2::data_t<operation> value) {
|
||||
// Determine from the TED video subsystem the length of this clock cycle as perceived by the 6502,
|
||||
// relative to the master clock.
|
||||
const auto length = video_.cycle_length(operation == CPU::MOS6502::BusOperation::Ready);
|
||||
const auto length = video_.cycle_length(operation == CPU::MOS6502Mk2::BusOperation::Ready);
|
||||
|
||||
// Update other subsystems.
|
||||
advance_timers_and_tape(length);
|
||||
@@ -261,11 +256,10 @@ public:
|
||||
c1541_->run_for(c1541_cycles_.divide(media_divider_));
|
||||
}
|
||||
|
||||
|
||||
time_since_audio_update_ += length;
|
||||
audio_ += length;
|
||||
}
|
||||
|
||||
if(operation == CPU::MOS6502::BusOperation::Ready) {
|
||||
if(operation == CPU::MOS6502Mk2::BusOperation::Ready) {
|
||||
return length;
|
||||
}
|
||||
|
||||
@@ -282,17 +276,17 @@ public:
|
||||
// b1 = serial clock out and cassette write;
|
||||
// b0 = serial data out.
|
||||
|
||||
if(is_read(operation)) {
|
||||
if constexpr (is_read(operation)) {
|
||||
if(!address) {
|
||||
*value = io_direction_;
|
||||
value = io_direction_;
|
||||
} else {
|
||||
*value = io_input();
|
||||
value = io_input();
|
||||
}
|
||||
} else {
|
||||
if(!address) {
|
||||
io_direction_ = *value;
|
||||
io_direction_ = value;
|
||||
} else {
|
||||
io_output_ = *value;
|
||||
io_output_ = value;
|
||||
}
|
||||
|
||||
const auto output = io_output_ | ~io_direction_;
|
||||
@@ -323,36 +317,36 @@ public:
|
||||
// );
|
||||
// }
|
||||
|
||||
if(
|
||||
use_fast_tape_hack_ &&
|
||||
operation == CPU::MOS6502Esque::BusOperation::ReadOpcode &&
|
||||
(
|
||||
(use_hle && address == 0xe5fd) ||
|
||||
address == 0xe68b ||
|
||||
address == 0xe68d
|
||||
)
|
||||
) {
|
||||
// ++pulse_num_;
|
||||
if(use_hle) {
|
||||
read_dipole();
|
||||
}
|
||||
if constexpr (is_read(operation)) {
|
||||
if(
|
||||
use_fast_tape_hack_ &&
|
||||
operation == CPU::MOS6502Mk2::BusOperation::ReadOpcode &&
|
||||
(
|
||||
(use_hle && address == 0xe5fd) ||
|
||||
address == 0xe68b ||
|
||||
address == 0xe68d
|
||||
)
|
||||
) {
|
||||
// ++pulse_num_;
|
||||
if(use_hle) {
|
||||
read_dipole();
|
||||
}
|
||||
|
||||
// using Flag = CPU::MOS6502::Flag;
|
||||
// using Register = CPU::MOS6502::Register;
|
||||
// const auto flags = m6502_.value_of(Register::Flags);
|
||||
// printf("to %lld: %c%c%c\n",
|
||||
// tape_player_->serialiser()->offset(),
|
||||
// flags & Flag::Sign ? 'n' : '-',
|
||||
// flags & Flag::Overflow ? 'v' : '-',
|
||||
// flags & Flag::Carry ? 'c' : '-'
|
||||
// );
|
||||
*value = 0x60;
|
||||
} else {
|
||||
if(is_read(operation)) {
|
||||
*value = map_.read(address);
|
||||
// using Flag = CPU::MOS6502::Flag;
|
||||
// using Register = CPU::MOS6502::Register;
|
||||
// const auto flags = m6502_.value_of(Register::Flags);
|
||||
// printf("to %lld: %c%c%c\n",
|
||||
// tape_player_->serialiser()->offset(),
|
||||
// flags & Flag::Sign ? 'n' : '-',
|
||||
// flags & Flag::Overflow ? 'v' : '-',
|
||||
// flags & Flag::Carry ? 'c' : '-'
|
||||
// );
|
||||
value = 0x60;
|
||||
} else {
|
||||
map_.write(address) = *value;
|
||||
value = map_.read(address);
|
||||
}
|
||||
} else {
|
||||
map_.write(address) = value;
|
||||
}
|
||||
|
||||
|
||||
@@ -393,12 +387,12 @@ public:
|
||||
// ram_[0x90] = 0;
|
||||
// ram_[0x93] = 0;
|
||||
//
|
||||
// *value = 0x0c; // NOP abs.
|
||||
// value = 0x0c; // NOP abs.
|
||||
// }
|
||||
// }
|
||||
} else if(address < 0xff00) {
|
||||
// Miscellaneous hardware. All TODO.
|
||||
if(is_read(operation)) {
|
||||
if constexpr (is_read(operation)) {
|
||||
switch(address & 0xfff0) {
|
||||
case 0xfd10:
|
||||
// 6529 parallel port, about which I know only what I've found in kernel ROM disassemblies.
|
||||
@@ -406,7 +400,7 @@ public:
|
||||
// If play button is not currently pressed and this read is immediately followed by
|
||||
// an AND 4, press it. The kernel will deal with motor control subsequently.
|
||||
if(!play_button_) {
|
||||
const uint16_t pc = m6502_.value_of(CPU::MOS6502::Register::ProgramCounter);
|
||||
const uint16_t pc = m6502_.registers().pc.full;
|
||||
const uint8_t next[] = {
|
||||
map_.read(pc+0),
|
||||
map_.read(pc+1),
|
||||
@@ -422,22 +416,23 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
*value = 0xff ^ (play_button_ ? 0x4 :0x0);
|
||||
value = 0xff ^ (play_button_ ? 0x4 :0x0);
|
||||
break;
|
||||
|
||||
case 0xfdd0:
|
||||
case 0xfdf0:
|
||||
*value = uint8_t(address >> 8);
|
||||
value = uint8_t(address >> 8);
|
||||
break;
|
||||
|
||||
default:
|
||||
value = 0xff;
|
||||
Logger::info().append("TODO: read @ %04x", address);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch(address & 0xfff0) {
|
||||
case 0xfd30:
|
||||
keyboard_mask_ = *value;
|
||||
keyboard_mask_ = value;
|
||||
break;
|
||||
|
||||
case 0xfdd0: {
|
||||
@@ -447,28 +442,28 @@ public:
|
||||
} break;
|
||||
|
||||
default:
|
||||
Logger::info().append("TODO: write of %02x @ %04x", *value, address);
|
||||
Logger::info().append("TODO: write of %02x @ %04x", value, address);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const auto pc = m6502_.value_of(CPU::MOS6502::Register::ProgramCounter);
|
||||
const auto pc = m6502_.registers().pc.full;
|
||||
const bool is_from_rom =
|
||||
(rom_is_paged_ && pc >= 0x8000) ||
|
||||
(pc >= 0x400 && pc < 0x500) ||
|
||||
(pc >= 0x700 && pc < 0x800);
|
||||
bool is_hit = true;
|
||||
|
||||
if(is_read(operation)) {
|
||||
if constexpr (is_read(operation)) {
|
||||
switch(address) {
|
||||
case 0xff00: *value = timers_.read<0>(); break;
|
||||
case 0xff01: *value = timers_.read<1>(); break;
|
||||
case 0xff02: *value = timers_.read<2>(); break;
|
||||
case 0xff03: *value = timers_.read<3>(); break;
|
||||
case 0xff04: *value = timers_.read<4>(); break;
|
||||
case 0xff05: *value = timers_.read<5>(); break;
|
||||
case 0xff06: *value = video_.read<0xff06>(); break;
|
||||
case 0xff07: *value = video_.read<0xff07>(); break;
|
||||
case 0xff00: value = timers_.read<0>(); break;
|
||||
case 0xff01: value = timers_.read<1>(); break;
|
||||
case 0xff02: value = timers_.read<2>(); break;
|
||||
case 0xff03: value = timers_.read<3>(); break;
|
||||
case 0xff04: value = timers_.read<4>(); break;
|
||||
case 0xff05: value = timers_.read<5>(); break;
|
||||
case 0xff06: value = video_.read<0xff06>(); break;
|
||||
case 0xff07: value = video_.read<0xff07>(); break;
|
||||
case 0xff08: {
|
||||
const uint8_t keyboard_input =
|
||||
~(
|
||||
@@ -487,127 +482,122 @@ public:
|
||||
((joystick_mask_ & 0x02) ? 0xff : (joystick(0).mask() | 0x40)) &
|
||||
((joystick_mask_ & 0x04) ? 0xff : (joystick(1).mask() | 0x80));
|
||||
|
||||
*value = keyboard_input & joystick_mask;
|
||||
value = keyboard_input & joystick_mask;
|
||||
} break;
|
||||
case 0xff09: *value = interrupts_.status(); break;
|
||||
case 0xff09: value = interrupts_.status(); break;
|
||||
case 0xff0a:
|
||||
*value = interrupts_.mask() | video_.read<0xff0a>() | 0xa0;
|
||||
value = interrupts_.mask() | video_.read<0xff0a>() | 0xa0;
|
||||
break;
|
||||
case 0xff0b: *value = video_.read<0xff0b>(); break;
|
||||
case 0xff0c: *value = video_.read<0xff0c>(); break;
|
||||
case 0xff0d: *value = video_.read<0xff0d>(); break;
|
||||
case 0xff0e: *value = ff0e_; break;
|
||||
case 0xff0f: *value = ff0f_; break;
|
||||
case 0xff10: *value = ff10_ | 0xfc; break;
|
||||
case 0xff11: *value = ff11_; break;
|
||||
case 0xff12: *value = ff12_ | 0xc0; break;
|
||||
case 0xff13: *value = ff13_ | (rom_is_paged_ ? 1 : 0); break;
|
||||
case 0xff14: *value = video_.read<0xff14>(); break;
|
||||
case 0xff15: *value = video_.read<0xff15>(); break;
|
||||
case 0xff16: *value = video_.read<0xff16>(); break;
|
||||
case 0xff17: *value = video_.read<0xff17>(); break;
|
||||
case 0xff18: *value = video_.read<0xff18>(); break;
|
||||
case 0xff19: *value = video_.read<0xff19>(); break;
|
||||
case 0xff1a: *value = video_.read<0xff1a>(); break;
|
||||
case 0xff1b: *value = video_.read<0xff1b>(); break;
|
||||
case 0xff1c: *value = video_.read<0xff1c>(); break;
|
||||
case 0xff1d: *value = video_.read<0xff1d>(); break;
|
||||
case 0xff1e: *value = video_.read<0xff1e>(); break;
|
||||
case 0xff1f: *value = video_.read<0xff1f>(); break;
|
||||
case 0xff0b: value = video_.read<0xff0b>(); break;
|
||||
case 0xff0c: value = video_.read<0xff0c>(); break;
|
||||
case 0xff0d: value = video_.read<0xff0d>(); break;
|
||||
case 0xff0e: value = ff0e_; break;
|
||||
case 0xff0f: value = ff0f_; break;
|
||||
case 0xff10: value = ff10_ | 0xfc; break;
|
||||
case 0xff11: value = ff11_; break;
|
||||
case 0xff12: value = ff12_ | 0xc0; break;
|
||||
case 0xff13: value = ff13_ | (rom_is_paged_ ? 1 : 0); break;
|
||||
case 0xff14: value = video_.read<0xff14>(); break;
|
||||
case 0xff15: value = video_.read<0xff15>(); break;
|
||||
case 0xff16: value = video_.read<0xff16>(); break;
|
||||
case 0xff17: value = video_.read<0xff17>(); break;
|
||||
case 0xff18: value = video_.read<0xff18>(); break;
|
||||
case 0xff19: value = video_.read<0xff19>(); break;
|
||||
case 0xff1a: value = video_.read<0xff1a>(); break;
|
||||
case 0xff1b: value = video_.read<0xff1b>(); break;
|
||||
case 0xff1c: value = video_.read<0xff1c>(); break;
|
||||
case 0xff1d: value = video_.read<0xff1d>(); break;
|
||||
case 0xff1e: value = video_.read<0xff1e>(); break;
|
||||
case 0xff1f: value = video_.read<0xff1f>(); break;
|
||||
|
||||
case 0xff3e: *value = 0; break;
|
||||
case 0xff3f: *value = 0; break;
|
||||
case 0xff3e: value = 0; break;
|
||||
case 0xff3f: value = 0; break;
|
||||
|
||||
default:
|
||||
Logger::info().append("TODO: TED read at %04x", address);
|
||||
value = 0xff;
|
||||
is_hit = false;
|
||||
}
|
||||
} else {
|
||||
switch(address) {
|
||||
case 0xff00: timers_.write<0>(*value); break;
|
||||
case 0xff01: timers_.write<1>(*value); break;
|
||||
case 0xff02: timers_.write<2>(*value); break;
|
||||
case 0xff03: timers_.write<3>(*value); break;
|
||||
case 0xff04: timers_.write<4>(*value); break;
|
||||
case 0xff05: timers_.write<5>(*value); break;
|
||||
case 0xff06: video_.write<0xff06>(*value); break;
|
||||
case 0xff00: timers_.write<0>(value); break;
|
||||
case 0xff01: timers_.write<1>(value); break;
|
||||
case 0xff02: timers_.write<2>(value); break;
|
||||
case 0xff03: timers_.write<3>(value); break;
|
||||
case 0xff04: timers_.write<4>(value); break;
|
||||
case 0xff05: timers_.write<5>(value); break;
|
||||
case 0xff06: video_.write<0xff06>(value); break;
|
||||
case 0xff07:
|
||||
video_.write<0xff07>(*value);
|
||||
update_audio();
|
||||
audio_.set_divider(*value);
|
||||
video_.write<0xff07>(value);
|
||||
audio_->set_divider(value);
|
||||
break;
|
||||
case 0xff08:
|
||||
// Observation here: the kernel posts a 0 to this
|
||||
// address upon completing each keyboard scan cycle,
|
||||
// once per frame.
|
||||
if(typer_ && !*value) {
|
||||
if(typer_ && !value) {
|
||||
if(!typer_->type_next_character()) {
|
||||
clear_all_keys();
|
||||
typer_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
joystick_mask_ = *value;
|
||||
joystick_mask_ = value;
|
||||
break;
|
||||
case 0xff09:
|
||||
interrupts_.set_status(*value);
|
||||
interrupts_.set_status(value);
|
||||
break;
|
||||
case 0xff0a:
|
||||
interrupts_.set_mask(*value);
|
||||
video_.write<0xff0a>(*value);
|
||||
interrupts_.set_mask(value);
|
||||
video_.write<0xff0a>(value);
|
||||
break;
|
||||
case 0xff0b: video_.write<0xff0b>(*value); break;
|
||||
case 0xff0c: video_.write<0xff0c>(*value); break;
|
||||
case 0xff0d: video_.write<0xff0d>(*value); break;
|
||||
case 0xff0b: video_.write<0xff0b>(value); break;
|
||||
case 0xff0c: video_.write<0xff0c>(value); break;
|
||||
case 0xff0d: video_.write<0xff0d>(value); break;
|
||||
case 0xff0e:
|
||||
ff0e_ = *value;
|
||||
update_audio();
|
||||
audio_.set_frequency_low<0>(*value);
|
||||
ff0e_ = value;
|
||||
audio_->set_frequency_low<0>(value);
|
||||
break;
|
||||
case 0xff0f:
|
||||
ff0f_ = *value;
|
||||
update_audio();
|
||||
audio_.set_frequency_low<1>(*value);
|
||||
ff0f_ = value;
|
||||
audio_->set_frequency_low<1>(value);
|
||||
break;
|
||||
case 0xff10:
|
||||
ff10_ = *value;
|
||||
update_audio();
|
||||
audio_.set_frequency_high<1>(*value);
|
||||
ff10_ = value;
|
||||
audio_->set_frequency_high<1>(value);
|
||||
break;
|
||||
case 0xff11:
|
||||
ff11_ = *value;
|
||||
update_audio();
|
||||
audio_.set_control(*value);
|
||||
ff11_ = value;
|
||||
audio_->set_control(value);
|
||||
break;
|
||||
case 0xff12:
|
||||
ff12_ = *value & 0x3f;
|
||||
video_.write<0xff12>(*value);
|
||||
ff12_ = value & 0x3f;
|
||||
video_.write<0xff12>(value);
|
||||
|
||||
if((*value & 4)) {
|
||||
if((value & 4)) {
|
||||
page_video_rom();
|
||||
} else {
|
||||
page_video_ram();
|
||||
}
|
||||
|
||||
update_audio();
|
||||
audio_.set_frequency_high<0>(*value);
|
||||
audio_->set_frequency_high<0>(value);
|
||||
break;
|
||||
case 0xff13:
|
||||
ff13_ = *value & 0xfe;
|
||||
video_.write<0xff13>(*value);
|
||||
ff13_ = value & 0xfe;
|
||||
video_.write<0xff13>(value);
|
||||
break;
|
||||
case 0xff14: video_.write<0xff14>(*value); break;
|
||||
case 0xff15: video_.write<0xff15>(*value); break;
|
||||
case 0xff16: video_.write<0xff16>(*value); break;
|
||||
case 0xff17: video_.write<0xff17>(*value); break;
|
||||
case 0xff18: video_.write<0xff18>(*value); break;
|
||||
case 0xff19: video_.write<0xff19>(*value); break;
|
||||
case 0xff1a: video_.write<0xff1a>(*value); break;
|
||||
case 0xff1b: video_.write<0xff1b>(*value); break;
|
||||
case 0xff1c: video_.write<0xff1c>(*value); break;
|
||||
case 0xff1d: video_.write<0xff1d>(*value); break;
|
||||
case 0xff1e: video_.write<0xff1e>(*value); break;
|
||||
case 0xff1f: video_.write<0xff1f>(*value); break;
|
||||
case 0xff14: video_.write<0xff14>(value); break;
|
||||
case 0xff15: video_.write<0xff15>(value); break;
|
||||
case 0xff16: video_.write<0xff16>(value); break;
|
||||
case 0xff17: video_.write<0xff17>(value); break;
|
||||
case 0xff18: video_.write<0xff18>(value); break;
|
||||
case 0xff19: video_.write<0xff19>(value); break;
|
||||
case 0xff1a: video_.write<0xff1a>(value); break;
|
||||
case 0xff1b: video_.write<0xff1b>(value); break;
|
||||
case 0xff1c: video_.write<0xff1c>(value); break;
|
||||
case 0xff1d: video_.write<0xff1d>(value); break;
|
||||
case 0xff1e: video_.write<0xff1e>(value); break;
|
||||
case 0xff1f: video_.write<0xff1f>(value); break;
|
||||
|
||||
case 0xff3e: page_cpu_rom(); break;
|
||||
case 0xff3f: page_cpu_ram(); break;
|
||||
@@ -626,22 +616,26 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
using Processor = CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, true>;
|
||||
Processor m6502_;
|
||||
struct M6502Traits {
|
||||
static constexpr auto uses_ready_line = true;
|
||||
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::BetweenInstructions;
|
||||
using BusHandlerT = ConcreteMachine;
|
||||
};
|
||||
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
return &speaker_;
|
||||
return &audio_.speaker();
|
||||
}
|
||||
|
||||
void set_activity_observer(Activity::Observer *const observer) final {
|
||||
if(c1541_) c1541_->set_activity_observer(observer);
|
||||
}
|
||||
|
||||
void set_irq_line(bool active) override {
|
||||
m6502_.set_irq_line(active);
|
||||
void set_irq_line(const bool active) override {
|
||||
m6502_.template set<CPU::MOS6502Mk2::Line::IRQ>(active);
|
||||
}
|
||||
void set_ready_line(bool active) override {
|
||||
m6502_.set_ready_line(active);
|
||||
void set_ready_line(const bool active) override {
|
||||
m6502_.template set<CPU::MOS6502Mk2::Line::Ready>(active);
|
||||
}
|
||||
|
||||
void page_video_rom() {
|
||||
@@ -684,16 +678,12 @@ private:
|
||||
|
||||
void run_for(const Cycles cycles) final {
|
||||
m6502_.run_for(cycles);
|
||||
|
||||
// I don't know why.
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
audio_.perform();
|
||||
}
|
||||
|
||||
void flush_output(int outputs) override {
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
audio_.perform();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,14 +710,7 @@ private:
|
||||
Cycles timers_subcycles_;
|
||||
Timers timers_;
|
||||
Video video_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Audio audio_;
|
||||
Cycles time_since_audio_update_;
|
||||
Outputs::Speaker::PullLowpass<Audio> speaker_;
|
||||
void update_audio() {
|
||||
speaker_.run_for(audio_queue_, time_since_audio_update_.flush<Cycles>());
|
||||
}
|
||||
Outputs::Speaker::PullLowpassSpeakerQueue<Cycles, Audio> audio_;
|
||||
|
||||
// MARK: - MappedKeyboardMachine.
|
||||
MappedKeyboardMachine::KeyboardMapper *get_keyboard_mapper() override {
|
||||
@@ -788,26 +771,19 @@ private:
|
||||
// TODO: substantially simplify the below; at the minute it's a
|
||||
// literal transcription of the original as a simple first step.
|
||||
void read_dipole() {
|
||||
using Register = CPU::MOS6502::Register;
|
||||
using Flag = CPU::MOS6502::Flag;
|
||||
using Flag = CPU::MOS6502Mk2::Flag;
|
||||
|
||||
//
|
||||
// Get registers now and ensure they'll be written back at function exit.
|
||||
//
|
||||
CPU::MOS6502Esque::LazyFlags flags(uint8_t(m6502_.value_of(Register::Flags)));
|
||||
uint8_t x, y, a;
|
||||
uint8_t s = uint8_t(m6502_.value_of(Register::StackPointer));
|
||||
auto registers = m6502_.registers();
|
||||
struct ScopeGuard {
|
||||
ScopeGuard(std::function<void(void)> at_exit) : at_exit_(at_exit) {}
|
||||
~ScopeGuard() { at_exit_(); }
|
||||
private:
|
||||
std::function<void(void)> at_exit_;
|
||||
} registers([&] {
|
||||
m6502_.set_value_of(Register::Flags, flags.get());
|
||||
m6502_.set_value_of(Register::A, a);
|
||||
m6502_.set_value_of(Register::X, x);
|
||||
m6502_.set_value_of(Register::Y, y);
|
||||
m6502_.set_value_of(Register::StackPointer, s);
|
||||
} store_registers([&] {
|
||||
m6502_.set_registers(registers);
|
||||
});
|
||||
|
||||
//
|
||||
@@ -822,38 +798,38 @@ private:
|
||||
// 6502 pseudo-ops.
|
||||
//
|
||||
const auto ldabs = [&] (uint8_t &target, const uint16_t address) {
|
||||
flags.set_nz(target = map_.read(address));
|
||||
registers.flags.set_per<Flag::NegativeZero>(target = map_.read(address));
|
||||
};
|
||||
const auto ldimm = [&] (uint8_t &target, const uint8_t value) {
|
||||
flags.set_nz(target = value);
|
||||
registers.flags.set_per<Flag::NegativeZero>(target = value);
|
||||
};
|
||||
const auto pha = [&] () {
|
||||
map_.write(0x100 + s) = a;
|
||||
--s;
|
||||
map_.write(0x100 + registers.s) = registers.a;
|
||||
--registers.s;
|
||||
};
|
||||
const auto pla = [&] () {
|
||||
++s;
|
||||
a = map_.read(0x100 + s);
|
||||
++registers.s;
|
||||
registers.a = map_.read(0x100 + registers.s);
|
||||
};
|
||||
const auto bit = [&] (const uint8_t value) {
|
||||
flags.zero_result = a & value;
|
||||
flags.negative_result = value;
|
||||
flags.overflow = value & CPU::MOS6502Esque::Flag::Overflow;
|
||||
registers.flags.set_per<Flag::Zero>(registers.a & value);
|
||||
registers.flags.set_per<Flag::Negative>(value);
|
||||
registers.flags.set_per<Flag::Overflow>(value);
|
||||
};
|
||||
const auto cmp = [&] (const uint8_t value) {
|
||||
const uint16_t temp16 = a - value;
|
||||
flags.set_nz(uint8_t(temp16));
|
||||
flags.carry = ((~temp16) >> 8)&1;
|
||||
const uint16_t temp16 = registers.a - value;
|
||||
registers.flags.set_per<Flag::NegativeZero>(uint8_t(temp16));
|
||||
registers.flags.set_per<Flag::Carry>(((~temp16) >> 8)&1);
|
||||
};
|
||||
const auto andimm = [&] (const uint8_t value) {
|
||||
a &= value;
|
||||
flags.set_nz(a);
|
||||
registers.a &= value;
|
||||
registers.flags.set_per<Flag::NegativeZero>(registers.a);
|
||||
};
|
||||
const auto ne = [&]() -> bool {
|
||||
return flags.zero_result;
|
||||
return !registers.flags.get<Flag::Zero>();
|
||||
};
|
||||
const auto eq = [&]() -> bool {
|
||||
return !flags.zero_result;
|
||||
return registers.flags.get<Flag::Zero>();
|
||||
};
|
||||
|
||||
//
|
||||
@@ -862,7 +838,7 @@ private:
|
||||
const auto dipok = [&] {
|
||||
// clc ; everything's fine
|
||||
// rts
|
||||
flags.carry = 0;
|
||||
registers.flags.set_per<Flag::Carry>(0);
|
||||
};
|
||||
const auto rshort = [&] {
|
||||
// bit tshrtd ; got a short
|
||||
@@ -878,7 +854,7 @@ private:
|
||||
const auto rderr1 = [&] {
|
||||
// sec ; i'm confused
|
||||
// rts
|
||||
flags.carry = Flag::Carry;
|
||||
registers.flags.set_per<Flag::Carry>(Flag::Carry);
|
||||
};
|
||||
|
||||
//
|
||||
@@ -891,8 +867,8 @@ private:
|
||||
//rddipl
|
||||
// ldx dsamp1 ; setup x,y with 1st sample point
|
||||
// ldy dsamp1+1
|
||||
ldabs(x, dsamp1);
|
||||
ldabs(y, dsamp1 + 1);
|
||||
ldabs(registers.x, dsamp1);
|
||||
ldabs(registers.y, dsamp1 + 1);
|
||||
advance_cycles(8);
|
||||
|
||||
//badeg1
|
||||
@@ -901,9 +877,9 @@ private:
|
||||
// pha
|
||||
// lda dsamp2
|
||||
// pha
|
||||
ldabs(a, dsamp2 + 1);
|
||||
ldabs(registers.a, dsamp2 + 1);
|
||||
pha();
|
||||
ldabs(a, dsamp2);
|
||||
ldabs(registers.a, dsamp2);
|
||||
pha();
|
||||
advance_cycles(14);
|
||||
|
||||
@@ -911,7 +887,7 @@ private:
|
||||
//rwtl ; wait till rd line is high
|
||||
// bit port [= $0001]
|
||||
// beq rwtl ; !ls!
|
||||
ldimm(a, 0x10);
|
||||
ldimm(registers.a, 0x10);
|
||||
advance_cycles(2);
|
||||
do {
|
||||
bit(io_input());
|
||||
@@ -933,8 +909,8 @@ private:
|
||||
|
||||
// stx timr2l
|
||||
// sty timr2h
|
||||
timers_.write<2>(x);
|
||||
timers_.write<3>(y);
|
||||
timers_.write<2>(registers.x);
|
||||
timers_.write<3>(registers.y);
|
||||
advance_cycles(8);
|
||||
|
||||
|
||||
@@ -945,9 +921,9 @@ private:
|
||||
// pla
|
||||
// sta timr3h ;go! ...tb
|
||||
pla();
|
||||
timers_.write<4>(a);
|
||||
timers_.write<4>(registers.a);
|
||||
pla();
|
||||
timers_.write<5>(a);
|
||||
timers_.write<5>(registers.a);
|
||||
advance_cycles(14);
|
||||
|
||||
|
||||
@@ -955,8 +931,8 @@ private:
|
||||
//
|
||||
// lda #$50 ; clr ta,tb
|
||||
// sta tedirq
|
||||
ldimm(a, 0x50);
|
||||
interrupts_.set_status(a);
|
||||
ldimm(registers.a, 0x50);
|
||||
interrupts_.set_status(registers.a);
|
||||
advance_cycles(6);
|
||||
|
||||
|
||||
@@ -969,7 +945,7 @@ private:
|
||||
// and #$10 ; a look at that edge again
|
||||
// bne badeg1 ; woa! got a bad edge trigger !ls!
|
||||
do {
|
||||
ldimm(a, io_input());
|
||||
ldimm(registers.a, io_input());
|
||||
cmp(io_input());
|
||||
if(advance_cycles(9)) {
|
||||
return;
|
||||
@@ -992,7 +968,7 @@ private:
|
||||
|
||||
// lda #$10
|
||||
//wata ; wait for ta to timeout
|
||||
ldimm(a, 0x10);
|
||||
ldimm(registers.a, 0x10);
|
||||
advance_cycles(3);
|
||||
do {
|
||||
// bit port ; kuldge, kludge, kludge !!! <<><>>
|
||||
@@ -1020,7 +996,7 @@ private:
|
||||
do {
|
||||
// lda port
|
||||
// cmp port
|
||||
ldimm(a, io_input());
|
||||
ldimm(registers.a, io_input());
|
||||
cmp(io_input());
|
||||
|
||||
if(advance_cycles(9)) {
|
||||
@@ -1048,7 +1024,7 @@ private:
|
||||
//
|
||||
//; wait for tb to timeout
|
||||
//; now do the dipole sample #2
|
||||
ldimm(a, 0x40);
|
||||
ldimm(registers.a, 0x40);
|
||||
advance_cycles(3);
|
||||
do {
|
||||
bit(interrupts_.status());
|
||||
@@ -1063,7 +1039,7 @@ private:
|
||||
// cmp port
|
||||
// bne casdb3
|
||||
do {
|
||||
ldimm(a, io_input());
|
||||
ldimm(registers.a, io_input());
|
||||
cmp(io_input());
|
||||
if(advance_cycles(9)) {
|
||||
return;
|
||||
@@ -1084,10 +1060,10 @@ private:
|
||||
// sta timr2l
|
||||
// lda zcell+1
|
||||
// sta timr2h
|
||||
ldabs(a, zcell);
|
||||
timers_.write<2>(a);
|
||||
ldabs(a, zcell + 1);
|
||||
timers_.write<3>(y);
|
||||
ldabs(registers.a, zcell);
|
||||
timers_.write<2>(registers.a);
|
||||
ldabs(registers.a, zcell + 1);
|
||||
timers_.write<3>(registers.y);
|
||||
advance_cycles(16);
|
||||
|
||||
|
||||
@@ -1096,9 +1072,9 @@ private:
|
||||
// lda #$10
|
||||
// sta tedirq ; verify +180 half of word dipole
|
||||
// lda #$10
|
||||
ldimm(a, 0x10);
|
||||
interrupts_.set_status(a);
|
||||
ldimm(a, 0x10);
|
||||
ldimm(registers.a, 0x10);
|
||||
interrupts_.set_status(registers.a);
|
||||
ldimm(registers.a, 0x10);
|
||||
advance_cycles(8);
|
||||
|
||||
//wata2
|
||||
@@ -1116,7 +1092,7 @@ private:
|
||||
// cmp port
|
||||
// bne casdb4
|
||||
do {
|
||||
ldimm(a, io_input());
|
||||
ldimm(registers.a, io_input());
|
||||
cmp(io_input());
|
||||
if(advance_cycles(9)) {
|
||||
return;
|
||||
@@ -1173,7 +1149,7 @@ private:
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->output = get_video_signal_configurable();
|
||||
options->quickload = allow_fast_tape_hack_;
|
||||
options->quick_load = allow_fast_tape_hack_;
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -1181,7 +1157,7 @@ private:
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
|
||||
set_video_signal_configurable(options->output);
|
||||
allow_fast_tape_hack_ = options->quickload;
|
||||
allow_fast_tape_hack_ = options->quick_load;
|
||||
set_use_fast_tape();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,19 +26,19 @@ struct Machine {
|
||||
|
||||
class Options:
|
||||
public Reflection::StructImpl<Options>,
|
||||
public Configurable::DisplayOption<Options>,
|
||||
public Configurable::QuickloadOption<Options>
|
||||
public Configurable::Options::Display<Options>,
|
||||
public Configurable::Options::QuickLoad<Options>
|
||||
{
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
friend Configurable::QuickloadOption<Options>;
|
||||
friend Configurable::Options::Display<Options>;
|
||||
friend Configurable::Options::QuickLoad<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::Options::Display<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Configurable::Display::SVideo : Configurable::Display::CompositeColour),
|
||||
Configurable::QuickloadOption<Options>(type == Configurable::OptionsType::UserFriendly) {
|
||||
Configurable::Options::QuickLoad<Options>(type == Configurable::OptionsType::UserFriendly) {
|
||||
}
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
|
||||
@@ -36,12 +36,11 @@ public:
|
||||
const auto visible_lines = 33 * 8;
|
||||
const auto centre = eos() - vs_stop() + 104; // i.e. centre on vertical_counter_ = 104.
|
||||
|
||||
crt_.set_visible_area(crt_.get_rect_for_area(
|
||||
crt_.set_fixed_framing(crt_.get_rect_for_area(
|
||||
centre - (visible_lines / 2),
|
||||
visible_lines,
|
||||
int(HorizontalEvent::Begin40Columns) - int(HorizontalEvent::BeginSync) + int(HorizontalEvent::ScheduleCounterReset) + 1 - 8,
|
||||
int(HorizontalEvent::End40Columns) - int(HorizontalEvent::Begin40Columns) + 16,
|
||||
4.0f / 3.0f
|
||||
int(HorizontalEvent::End40Columns) - int(HorizontalEvent::Begin40Columns) + 16
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -36,15 +36,15 @@ enum Key: uint16_t {
|
||||
Key9 = key(0, 0x10), KeyPlus = key(0, 0x20), KeyGBP = key(0, 0x40), KeyDelete = key(0, 0x80),
|
||||
|
||||
// Virtual keys.
|
||||
KeyUp = 0xfff0,
|
||||
KeyLeft = 0xfff1,
|
||||
KeyF2 = 0xfff2,
|
||||
KeyF4 = 0xfff3,
|
||||
KeyF6 = 0xfff4,
|
||||
KeyF8 = 0xfff5,
|
||||
KeyUp = 0xfe00,
|
||||
KeyLeft,
|
||||
KeyF2,
|
||||
KeyF4,
|
||||
KeyF6,
|
||||
KeyF8,
|
||||
|
||||
// Physical keys not within the usual matrix.
|
||||
KeyRestore = 0xfffc,
|
||||
KeyRestore,
|
||||
};
|
||||
|
||||
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "Activity/Source.hpp"
|
||||
#include "Machines/MachineTypes.hpp"
|
||||
|
||||
#include "Processors/6502/6502.hpp"
|
||||
#include "Processors/6502Mk2/6502Mk2.hpp"
|
||||
#include "Components/6560/6560.hpp"
|
||||
#include "Components/6522/6522.hpp"
|
||||
|
||||
@@ -72,26 +72,30 @@ public:
|
||||
// Port A provides information about the presence or absence of a tape, and parts of
|
||||
// the joystick and serial port state, both of which have been statefully collected
|
||||
// into port_a_.
|
||||
if(!port) {
|
||||
if constexpr (port == MOS::MOS6522::Port::A) {
|
||||
return port_a_ | (tape_->has_tape() ? 0x00 : 0x40);
|
||||
}
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
/// Receives announcements of control line output change from the 6522.
|
||||
template <MOS::MOS6522::Port port, MOS::MOS6522::Line line> void set_control_line_output(const bool value) {
|
||||
// The CA2 output is used to control the tape motor.
|
||||
if(port == MOS::MOS6522::Port::A && line == MOS::MOS6522::Line::Two) {
|
||||
template <MOS::MOS6522::Port port, MOS::MOS6522::Line line>
|
||||
void set_control_line_output(const bool value) {
|
||||
// CA2: control the tape motor.
|
||||
if constexpr (port == MOS::MOS6522::Port::A && line == MOS::MOS6522::Line::Two) {
|
||||
tape_->set_motor_control(!value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Receives announcements of changes in the serial bus connected to the serial port and propagates them into Port A.
|
||||
void set_serial_line_state(Commodore::Serial::Line line, const bool value) {
|
||||
void set_serial_line_state(const Commodore::Serial::Line line, const bool value) {
|
||||
const auto set = [&](const uint8_t bit) {
|
||||
port_a_ = (port_a_ & ~bit) | (value ? bit : 0x00);
|
||||
};
|
||||
switch(line) {
|
||||
default: break;
|
||||
case ::Commodore::Serial::Line::Data: port_a_ = (port_a_ & ~0x02) | (value ? 0x02 : 0x00); break;
|
||||
case ::Commodore::Serial::Line::Clock: port_a_ = (port_a_ & ~0x01) | (value ? 0x01 : 0x00); break;
|
||||
case ::Commodore::Serial::Line::Data: set(0x02); break;
|
||||
case ::Commodore::Serial::Line::Clock: set(0x01); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +150,7 @@ public:
|
||||
|
||||
/// Sets all keys as unpressed.
|
||||
void clear_all_keys() {
|
||||
memset(columns_, 0xff, sizeof(columns_));
|
||||
std::fill(std::begin(columns_), std::end(columns_), 0xff);
|
||||
}
|
||||
|
||||
/// Called by the 6522 to get input. Reads the keyboard on Port A, returns a small amount of joystick state on Port B.
|
||||
@@ -230,8 +234,6 @@ struct Vic6560BusHandler {
|
||||
// It is assumed that these pointers have been filled in by the machine.
|
||||
const uint8_t *video_memory_map[16]{}; // Segments video memory into 1kb portions.
|
||||
const uint8_t *colour_memory{}; // Colour memory must be contiguous.
|
||||
|
||||
// TODO: make the above const.
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -253,7 +255,7 @@ public:
|
||||
void did_set_input(const Input &digital_input, const bool is_active) final {
|
||||
if(const auto mapped_input = [&]() -> std::optional<JoystickInput> {
|
||||
switch(digital_input.type) {
|
||||
default: return std::nullopt;
|
||||
default: return std::nullopt;
|
||||
case Input::Up: return Up;
|
||||
case Input::Down: return Down;
|
||||
case Input::Left: return Left;
|
||||
@@ -280,7 +282,6 @@ class ConcreteMachine:
|
||||
public MachineTypes::MappedKeyboardMachine,
|
||||
public MachineTypes::JoystickMachine,
|
||||
public Configurable::Device,
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
|
||||
public Utility::TypeRecipient<CharacterMapper>,
|
||||
public Storage::Tape::BinaryTapePlayer::Delegate,
|
||||
@@ -455,7 +456,7 @@ public:
|
||||
}
|
||||
|
||||
void set_key_state(const uint16_t key, const bool is_pressed) final {
|
||||
if(key < 0xfff0) {
|
||||
if(key < KeyUp) {
|
||||
keyboard_via_port_handler_.set_key_state(key, is_pressed);
|
||||
} else {
|
||||
switch(key) {
|
||||
@@ -481,28 +482,27 @@ public:
|
||||
|
||||
void clear_all_keys() final {
|
||||
keyboard_via_port_handler_.clear_all_keys();
|
||||
set_key_state(KeyRestore, false);
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
// to satisfy CPU::MOS6502::Processor
|
||||
forceinline Cycles perform_bus_operation(
|
||||
const CPU::MOS6502::BusOperation operation,
|
||||
const uint16_t address,
|
||||
uint8_t *const value
|
||||
) {
|
||||
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
|
||||
Cycles perform(const AddressT address, CPU::MOS6502Mk2::data_t<operation> value) {
|
||||
// Tun the phase-1 part of this cycle, in which the VIC accesses memory.
|
||||
cycles_since_mos6560_update_++;
|
||||
|
||||
// Run the phase-2 part of the cycle, which is whatever the 6502 said it should be.
|
||||
const bool is_from_rom = m6502_.value_of(CPU::MOS6502::Register::ProgramCounter) > 0x8000;
|
||||
if(is_read(operation)) {
|
||||
const auto is_from_rom = [&]() {
|
||||
return m6502_.registers().pc.full > 0x8000;
|
||||
};
|
||||
if constexpr (is_read(operation)) {
|
||||
const auto page = processor_read_memory_map_[address >> 10];
|
||||
uint8_t result;
|
||||
if(!page) {
|
||||
if(!is_from_rom) confidence_.add_miss();
|
||||
if(!is_from_rom()) confidence_.add_miss();
|
||||
result = 0xff;
|
||||
} else {
|
||||
result = processor_read_memory_map_[address >> 10][address & 0x3ff];
|
||||
@@ -515,7 +515,7 @@ public:
|
||||
if(address & 0x10) result &= user_port_via_.read(address);
|
||||
if(address & 0x20) result &= keyboard_via_.read(address);
|
||||
|
||||
if(!is_from_rom) {
|
||||
if(!is_from_rom()) {
|
||||
if((address & 0x100) && !(address & 0x30)) {
|
||||
confidence_.add_miss();
|
||||
} else {
|
||||
@@ -523,10 +523,10 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
*value = result;
|
||||
value = result;
|
||||
|
||||
// Consider applying the fast tape hack.
|
||||
if(use_fast_tape_hack_ && operation == CPU::MOS6502::BusOperation::ReadOpcode) {
|
||||
if(use_fast_tape_hack_ && operation == CPU::MOS6502Mk2::BusOperation::ReadOpcode) {
|
||||
if(address == 0xf7b2) {
|
||||
// Address 0xf7b2 contains a JSR to 0xf8c0 ('RDTPBLKS') that will fill the tape buffer with the
|
||||
// next header. Skip that via a three-byte NOP and fill in the next header programmatically.
|
||||
@@ -551,10 +551,10 @@ public:
|
||||
ram_[0x90] = 0;
|
||||
ram_[0x93] = 0;
|
||||
|
||||
*value = 0x0c; // i.e. NOP abs, to swallow the entire JSR
|
||||
value = 0x0c; // i.e. NOP abs, to swallow the entire JSR
|
||||
} else if(address == 0xf90b) {
|
||||
const auto x = uint8_t(m6502_.value_of(CPU::MOS6502::Register::X));
|
||||
if(x == 0xe) {
|
||||
auto registers = m6502_.registers();
|
||||
if(registers.x == 0xe) {
|
||||
Storage::Tape::Commodore::Parser parser(TargetPlatform::Vic20);
|
||||
const auto tape_position = tape_->serialiser()->offset();
|
||||
const std::unique_ptr<Storage::Tape::Commodore::Data> data = parser.get_next_data(*tape_->serialiser());
|
||||
@@ -576,14 +576,13 @@ public:
|
||||
|
||||
// set tape status, carry and flag
|
||||
ram_[0x90] |= 0x40;
|
||||
uint8_t flags = uint8_t(m6502_.value_of(CPU::MOS6502::Register::Flags));
|
||||
flags &= ~uint8_t((CPU::MOS6502::Flag::Carry | CPU::MOS6502::Flag::Interrupt));
|
||||
m6502_.set_value_of(CPU::MOS6502::Register::Flags, flags);
|
||||
registers.flags.set_per<CPU::MOS6502Mk2::Flag::Carry>(0);
|
||||
registers.flags.set_per<CPU::MOS6502Mk2::Flag::Interrupt>(0);
|
||||
|
||||
// to ensure that execution proceeds to 0xfccf, pretend a NOP was here and
|
||||
// ensure that the PC leaps to 0xfccf
|
||||
m6502_.set_value_of(CPU::MOS6502::Register::ProgramCounter, 0xfccf);
|
||||
*value = 0xea; // i.e. NOP implied
|
||||
registers.pc.full = 0xfccf;
|
||||
value = 0xea; // i.e. NOP implied
|
||||
hold_tape_ = true;
|
||||
Logger::info().append("Found data");
|
||||
} else {
|
||||
@@ -592,27 +591,28 @@ public:
|
||||
Logger::info().append("Didn't find data");
|
||||
}
|
||||
}
|
||||
m6502_.set_registers(registers);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uint8_t *const ram = processor_write_memory_map_[address >> 10];
|
||||
if(ram) {
|
||||
update_video();
|
||||
ram[address & 0x3ff] = *value;
|
||||
ram[address & 0x3ff] = value;
|
||||
}
|
||||
// Anything between 0x9000 and 0x9400 is the IO area.
|
||||
if((address&0xfc00) == 0x9000) {
|
||||
// The VIC is selected by bit 8 = 0
|
||||
if(!(address&0x100)) {
|
||||
update_video();
|
||||
mos6560_.write(address, *value);
|
||||
mos6560_.write(address, value);
|
||||
}
|
||||
// The first VIA is selected by bit 4 = 1.
|
||||
if(address & 0x10) user_port_via_.write(address, *value);
|
||||
if(address & 0x10) user_port_via_.write(address, value);
|
||||
// The second VIA is selected by bit 5 = 1.
|
||||
if(address & 0x20) keyboard_via_.write(address, *value);
|
||||
if(address & 0x20) keyboard_via_.write(address, value);
|
||||
|
||||
if(!is_from_rom) {
|
||||
if(!is_from_rom()) {
|
||||
if((address & 0x100) && !(address & 0x30)) {
|
||||
confidence_.add_miss();
|
||||
} else {
|
||||
@@ -620,13 +620,13 @@ public:
|
||||
}
|
||||
}
|
||||
} else if(!ram) {
|
||||
if(!is_from_rom) confidence_.add_miss();
|
||||
if(!is_from_rom()) confidence_.add_miss();
|
||||
}
|
||||
}
|
||||
|
||||
user_port_via_.run_for(Cycles(1));
|
||||
keyboard_via_.run_for(Cycles(1));
|
||||
if(typer_ && address == 0xeb1e && operation == CPU::MOS6502::BusOperation::ReadOpcode) {
|
||||
if(typer_ && address == 0xeb1e && operation == CPU::MOS6502Mk2::BusOperation::ReadOpcode) {
|
||||
if(!typer_->type_next_character()) {
|
||||
clear_all_keys();
|
||||
typer_.reset();
|
||||
@@ -672,8 +672,8 @@ public:
|
||||
}
|
||||
|
||||
void mos6522_did_change_interrupt_status(void *) final {
|
||||
m6502_.set_nmi_line(user_port_via_.get_interrupt_line());
|
||||
m6502_.set_irq_line(keyboard_via_.get_interrupt_line());
|
||||
m6502_.template set<CPU::MOS6502Mk2::Line::NMI>(user_port_via_.get_interrupt_line());
|
||||
m6502_.template set<CPU::MOS6502Mk2::Line::IRQ>(keyboard_via_.get_interrupt_line());
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) final {
|
||||
@@ -696,14 +696,14 @@ public:
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->output = get_video_signal_configurable();
|
||||
options->quickload = allow_fast_tape_hack_;
|
||||
options->quick_load = allow_fast_tape_hack_;
|
||||
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);
|
||||
allow_fast_tape_hack_ = options->quickload;
|
||||
allow_fast_tape_hack_ = options->quick_load;
|
||||
set_use_fast_tape();
|
||||
}
|
||||
|
||||
@@ -718,10 +718,16 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
struct M6502Traits {
|
||||
static constexpr auto uses_ready_line = false;
|
||||
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::BetweenInstructions;
|
||||
using BusHandlerT = ConcreteMachine;
|
||||
};
|
||||
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
|
||||
|
||||
void update_video() {
|
||||
mos6560_.run_for(cycles_since_mos6560_update_.flush<Cycles>());
|
||||
}
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
|
||||
|
||||
std::vector<uint8_t> character_rom_;
|
||||
std::vector<uint8_t> basic_rom_;
|
||||
@@ -745,12 +751,22 @@ private:
|
||||
++address;
|
||||
}
|
||||
}
|
||||
void write_to_map(const uint8_t **const map, const uint8_t *area, uint16_t address, size_t length) {
|
||||
void write_to_map(
|
||||
const uint8_t **const map,
|
||||
const uint8_t *const area,
|
||||
const uint16_t address,
|
||||
const size_t length
|
||||
) {
|
||||
write_to_map([&](const uint16_t address, const size_t offset) {
|
||||
map[address] = &area[offset];
|
||||
}, address, length);
|
||||
}
|
||||
void write_to_map(uint8_t **const map, uint8_t *area, uint16_t address, size_t length) {
|
||||
void write_to_map(
|
||||
uint8_t **const map,
|
||||
uint8_t *const area,
|
||||
const uint16_t address,
|
||||
const size_t length
|
||||
) {
|
||||
write_to_map([&](const uint16_t address, const size_t offset) {
|
||||
map[address] = &area[offset];
|
||||
}, address, length);
|
||||
|
||||
@@ -28,16 +28,16 @@ struct Machine {
|
||||
|
||||
class Options:
|
||||
public Reflection::StructImpl<Options>,
|
||||
public Configurable::DisplayOption<Options>,
|
||||
public Configurable::QuickloadOption<Options>
|
||||
public Configurable::Options::Display<Options>,
|
||||
public Configurable::Options::QuickLoad<Options>
|
||||
{
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
friend Configurable::QuickloadOption<Options>;
|
||||
friend Configurable::Options::Display<Options>;
|
||||
friend Configurable::Options::QuickLoad<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::Options::Display<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Configurable::Display::SVideo : Configurable::Display::CompositeColour),
|
||||
Configurable::QuickloadOption<Options>(type == Configurable::OptionsType::UserFriendly) {
|
||||
Configurable::Options::QuickLoad<Options>(type == Configurable::OptionsType::UserFriendly) {
|
||||
}
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
@@ -12,7 +12,7 @@ using namespace Enterprise::Dave;
|
||||
|
||||
// MARK: - Audio generator
|
||||
|
||||
Audio::Audio(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
Audio::Audio(Outputs::Speaker::TaskQueue &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
void Audio::write(uint16_t address, const uint8_t value) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include <cstdint>
|
||||
|
||||
#include "ClockReceiver/ClockReceiver.hpp"
|
||||
#include "Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "Outputs/Speaker/SpeakerQueue.hpp"
|
||||
#include "Numeric/LFSR.hpp"
|
||||
#include "Outputs/Speaker/Implementation/BufferSource.hpp"
|
||||
|
||||
@@ -28,7 +28,7 @@ enum class Interrupt: uint8_t {
|
||||
*/
|
||||
class Audio: public Outputs::Speaker::BufferSource<Audio, true> {
|
||||
public:
|
||||
Audio(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
Audio(Outputs::Speaker::TaskQueue &);
|
||||
|
||||
/// 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
|
||||
@@ -41,7 +41,7 @@ public:
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::StereoSample *target);
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Outputs::Speaker::TaskQueue &audio_queue_;
|
||||
|
||||
// Global divider (i.e. 8MHz/12Mhz switch).
|
||||
uint8_t global_divider_;
|
||||
|
||||
@@ -103,12 +103,10 @@ public:
|
||||
min_ram_slot_(min_ram_slot(target)),
|
||||
z80_(*this),
|
||||
nick_(ram_.end() - 65536),
|
||||
dave_audio_(audio_queue_),
|
||||
speaker_(dave_audio_) {
|
||||
audio_(float(clock_rate) / float(DaveDivider), DaveDivider) {
|
||||
|
||||
// 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;
|
||||
@@ -257,7 +255,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
audio_.stop();
|
||||
}
|
||||
|
||||
// MARK: - Z80::BusHandler.
|
||||
@@ -344,7 +342,7 @@ public:
|
||||
}
|
||||
|
||||
const HalfCycles full_length = cycle.length + penalty;
|
||||
time_since_audio_update_ += full_length;
|
||||
audio_ += 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());
|
||||
@@ -475,8 +473,7 @@ public:
|
||||
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);
|
||||
audio_->write(address, *cycle.value);
|
||||
dave_timer_->write(address, *cycle.value);
|
||||
break;
|
||||
|
||||
@@ -563,8 +560,7 @@ public:
|
||||
nick_.flush();
|
||||
}
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
audio_.perform();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,7 +646,7 @@ private:
|
||||
// MARK: - AudioProducer
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
return &audio_.speaker();
|
||||
}
|
||||
|
||||
// MARK: - TimedMachine
|
||||
@@ -726,20 +722,13 @@ private:
|
||||
bool previous_nick_interrupt_line_ = false;
|
||||
// Cf. timing guesses above.
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Dave::Audio dave_audio_;
|
||||
Outputs::Speaker::PullLowpass<Dave::Audio> speaker_;
|
||||
HalfCycles time_since_audio_update_;
|
||||
|
||||
Outputs::Speaker::PullLowpassSpeakerQueue<HalfCycles, Dave::Audio> audio_;
|
||||
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)));
|
||||
}
|
||||
// the spekaer queue should match.
|
||||
static constexpr int DaveDivider = 8;
|
||||
JustInTimeActor<Dave::TimedInterruptSource, HalfCycles, 1, DaveDivider> dave_timer_;
|
||||
|
||||
// MARK: - EXDos card.
|
||||
EXDos exdos_;
|
||||
|
||||
@@ -28,16 +28,16 @@ struct Machine {
|
||||
static std::unique_ptr<Machine> Enterprise(const Analyser::Static::Target *, const ROMMachine::ROMFetcher &);
|
||||
|
||||
/// Defines the runtime options available for an Enterprise.
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::Options::Display<Options> {
|
||||
friend Configurable::Options::Display<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::Options::Display<Options>(
|
||||
type == Configurable::OptionsType::UserFriendly ?
|
||||
Configurable::Display::RGB : Configurable::Display::CompositeColour) {}
|
||||
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
|
||||
@@ -54,7 +54,7 @@ Nick::Nick(const uint8_t *const ram) :
|
||||
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));
|
||||
crt_.set_fixed_framing(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f));
|
||||
}
|
||||
|
||||
void Nick::write(const uint16_t address, const uint8_t value) {
|
||||
|
||||
@@ -360,7 +360,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
speaker_.audio_queue.flush();
|
||||
speaker_.audio_queue.lock_flush();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
@@ -815,14 +815,14 @@ public:
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->output = get_video_signal_configurable();
|
||||
options->quickload = allow_fast_tape_;
|
||||
options->quick_load = allow_fast_tape_;
|
||||
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);
|
||||
allow_fast_tape_ = options->quickload;
|
||||
allow_fast_tape_ = options->quick_load;
|
||||
set_use_fast_tape();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,18 +23,18 @@ struct Machine {
|
||||
|
||||
class Options:
|
||||
public Reflection::StructImpl<Options>,
|
||||
public Configurable::DisplayOption<Options>,
|
||||
public Configurable::QuickloadOption<Options>
|
||||
public Configurable::Options::Display<Options>,
|
||||
public Configurable::Options::QuickLoad<Options>
|
||||
{
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
friend Configurable::QuickloadOption<Options>;
|
||||
friend Configurable::Options::Display<Options>;
|
||||
friend Configurable::Options::QuickLoad<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ? Configurable::Display::RGB : Configurable::Display::CompositeColour),
|
||||
Configurable::QuickloadOption<Options>(type == Configurable::OptionsType::UserFriendly) {}
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::Options::Display<Options>(type == Configurable::OptionsType::UserFriendly ? Configurable::Display::RGB : Configurable::Display::CompositeColour),
|
||||
Configurable::Options::QuickLoad<Options>(type == Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
|
||||
@@ -178,7 +178,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
audio_queue_.lock_flush();
|
||||
}
|
||||
|
||||
ChangeEffect effect_for_file_did_change(const std::string &) const final {
|
||||
|
||||
@@ -21,15 +21,15 @@ struct Machine {
|
||||
virtual ~Machine() = default;
|
||||
static std::unique_ptr<Machine> MasterSystem(const Analyser::Static::Target *, const ROMMachine::ROMFetcher &);
|
||||
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::Options::Display<Options> {
|
||||
friend Configurable::Options::Display<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::Options::Display<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Configurable::Display::RGB : Configurable::Display::CompositeColour) {}
|
||||
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
|
||||
@@ -31,8 +31,8 @@ enum Key: uint16_t {
|
||||
KeyEquals = 0x0700 | 0x80, KeyReturn = 0x0700 | 0x20, KeyRightShift = 0x0700 | 0x10,
|
||||
KeyForwardSlash = 0x0700 | 0x08, Key0 = 0x0700 | 0x04, KeyL = 0x0700 | 0x02, Key8 = 0x0700 | 0x01,
|
||||
|
||||
KeyNMI = 0xfffc,
|
||||
KeyJasminReset = 0xfffb,
|
||||
KeyNMI = 0xfe00,
|
||||
KeyJasminReset,
|
||||
};
|
||||
|
||||
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
|
||||
@@ -406,7 +406,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
audio_queue_.lock_flush();
|
||||
}
|
||||
|
||||
void set_key_state(uint16_t key, bool is_pressed) final {
|
||||
@@ -679,14 +679,14 @@ public:
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->output = get_video_signal_configurable();
|
||||
options->quickload = use_fast_tape_hack_;
|
||||
options->quick_load = use_fast_tape_hack_;
|
||||
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);
|
||||
set_use_fast_tape_hack(options->quickload);
|
||||
set_use_fast_tape_hack(options->quick_load);
|
||||
}
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) final {
|
||||
|
||||
@@ -26,19 +26,19 @@ struct Machine {
|
||||
|
||||
class Options:
|
||||
public Reflection::StructImpl<Options>,
|
||||
public Configurable::DisplayOption<Options>,
|
||||
public Configurable::QuickloadOption<Options>
|
||||
public Configurable::Options::Display<Options>,
|
||||
public Configurable::Options::QuickLoad<Options>
|
||||
{
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
friend Configurable::QuickloadOption<Options>;
|
||||
friend Configurable::Options::Display<Options>;
|
||||
friend Configurable::Options::QuickLoad<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Options(const Configurable::OptionsType type) :
|
||||
Configurable::Options::Display<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Configurable::Display::RGB : Configurable::Display::CompositeColour),
|
||||
Configurable::QuickloadOption<Options>(type == Configurable::OptionsType::UserFriendly) {}
|
||||
Configurable::Options::QuickLoad<Options>(type == Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
|
||||
@@ -34,6 +34,11 @@ VideoOutput::VideoOutput(uint8_t *memory) :
|
||||
crt_.set_input_data_type(data_type_);
|
||||
crt_.set_delegate(&frequency_mismatch_warner_);
|
||||
update_crt_frequency();
|
||||
|
||||
// Prewarm CRT.
|
||||
crt_.set_fixed_framing([&] {
|
||||
run_for(Cycles(10'000));
|
||||
});
|
||||
}
|
||||
|
||||
void VideoOutput::register_crt_frequency_mismatch() {
|
||||
@@ -42,11 +47,7 @@ void VideoOutput::register_crt_frequency_mismatch() {
|
||||
}
|
||||
|
||||
void VideoOutput::update_crt_frequency() {
|
||||
// Set the proper frequency...
|
||||
crt_.set_new_display_type(64*6, crt_is_60Hz_ ? Outputs::Display::Type::PAL60 : Outputs::Display::Type::PAL50);
|
||||
|
||||
// ... but also pick an appropriate crop rectangle.
|
||||
crt_.set_visible_area(crt_.get_rect_for_area(crt_is_60Hz_ ? 26 : 54, 224, 16 * 6, 40 * 6, 4.0f / 3.0f));
|
||||
}
|
||||
|
||||
void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Outputs/CRT/CRT.hpp"
|
||||
#include "Outputs/CRT/MismatchWarner.hpp"
|
||||
#include "ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -103,7 +103,6 @@ private:
|
||||
CRTCOutputter() :
|
||||
crt(910, 8, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red2Green2Blue2)
|
||||
{
|
||||
crt.set_visible_area(Outputs::Display::Rect(0.095f, 0.095f, 0.82f, 0.82f));
|
||||
crt.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
}
|
||||
|
||||
@@ -192,11 +191,7 @@ private:
|
||||
switch(output_state) {
|
||||
case OutputState::Sync: crt.output_sync(count * active_clock_divider); break;
|
||||
case OutputState::Border:
|
||||
if(active_border_colour) {
|
||||
crt.output_blank(count * active_clock_divider);
|
||||
} else {
|
||||
crt.output_level<uint8_t>(count * active_clock_divider, active_border_colour);
|
||||
}
|
||||
crt.output_level<uint8_t>(count * active_clock_divider, active_border_colour);
|
||||
break;
|
||||
case OutputState::ColourBurst: crt.output_colour_burst(count * active_clock_divider, colour_phase); break;
|
||||
case OutputState::Pixels: flush_pixels(); break;
|
||||
|
||||
@@ -104,9 +104,9 @@ public:
|
||||
return channels_[channel].address.halves.low;
|
||||
}
|
||||
}
|
||||
}
|
||||
case 0x8: return status();
|
||||
case 0xd: return temporary_register();
|
||||
}
|
||||
case 0x8: return status();
|
||||
case 0xd: return temporary_register();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ public:
|
||||
// Status:
|
||||
// b7 = 1 => parity error on transmission;
|
||||
// b6 = 1 => receive timeout;
|
||||
// b5 = 1 => transmit timeout;
|
||||
// b5 = 1 => transmit timeout;
|
||||
// b4 = 1 => keyboard enabled via physical key;
|
||||
// b3 = 1 = data at 0060 is command, 0 = data;
|
||||
// b2 = 1 = selftest OK; 0 = just powered up or reset;
|
||||
@@ -328,7 +328,7 @@ private:
|
||||
|
||||
switch(command) {
|
||||
case Command::SelfTest: return 15;
|
||||
default: return 0;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,11 +448,11 @@ private:
|
||||
bool has_input_ = false;
|
||||
bool has_command_ = false;
|
||||
|
||||
// bit 7 = 0 keyboard inhibited
|
||||
// bit 6 = 0 CGA, else MDA
|
||||
// bit 5 = 0 manufacturing jumper installed
|
||||
// bit 4 = 0 system RAM 512K, else 640K
|
||||
// bit 3-0 reserved
|
||||
// bit 7 = 0 keyboard inhibited
|
||||
// bit 6 = 0 CGA, else MDA
|
||||
// bit 5 = 0 manufacturing jumper installed
|
||||
// bit 4 = 0 system RAM 512K, else 640K
|
||||
// bit 3-0 reserved
|
||||
uint8_t switches_ = 0b1011'0000;
|
||||
|
||||
int perform_delay_ = 0;
|
||||
|
||||
@@ -96,7 +96,6 @@ private:
|
||||
// TODO: really this should be a Luminance8 and set an appropriate modal tint colour;
|
||||
// consider whether that's worth building into the scan target.
|
||||
{
|
||||
crt.set_visible_area(Outputs::Display::Rect(0.028f, 0.025f, 0.98f, 0.98f));
|
||||
crt.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
}
|
||||
|
||||
|
||||
@@ -540,14 +540,14 @@ private:
|
||||
case 0x0086: return dma_.pages.template page<6>();
|
||||
case 0x0087: return dma_.pages.template page<7>();
|
||||
|
||||
case 0x0088: if(require_at(port)) return dma_.pages.template page<0x8>(); break;
|
||||
case 0x0089: if(require_at(port)) return dma_.pages.template page<0x9>(); break;
|
||||
case 0x008a: if(require_at(port)) return dma_.pages.template page<0xa>(); break;
|
||||
case 0x008b: if(require_at(port)) return dma_.pages.template page<0xb>(); break;
|
||||
case 0x008c: if(require_at(port)) return dma_.pages.template page<0xc>(); break;
|
||||
case 0x008d: if(require_at(port)) return dma_.pages.template page<0xd>(); break;
|
||||
case 0x008e: if(require_at(port)) return dma_.pages.template page<0xe>(); break;
|
||||
case 0x008f: if(require_at(port)) return dma_.pages.template page<0xf>(); break;
|
||||
case 0x0088: if(require_at(port)) return dma_.pages.template page<0x8>(); break;
|
||||
case 0x0089: if(require_at(port)) return dma_.pages.template page<0x9>(); break;
|
||||
case 0x008a: if(require_at(port)) return dma_.pages.template page<0xa>(); break;
|
||||
case 0x008b: if(require_at(port)) return dma_.pages.template page<0xb>(); break;
|
||||
case 0x008c: if(require_at(port)) return dma_.pages.template page<0xc>(); break;
|
||||
case 0x008d: if(require_at(port)) return dma_.pages.template page<0xd>(); break;
|
||||
case 0x008e: if(require_at(port)) return dma_.pages.template page<0xe>(); break;
|
||||
case 0x008f: if(require_at(port)) return dma_.pages.template page<0xf>(); break;
|
||||
|
||||
case 0x03f4: return fdc_.status();
|
||||
case 0x03f5: return fdc_.read();
|
||||
@@ -817,7 +817,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
speaker_.queue.flush();
|
||||
speaker_.queue.lock_flush();
|
||||
}
|
||||
|
||||
// MARK: - TimedMachine.
|
||||
|
||||
@@ -30,15 +30,15 @@ struct Machine {
|
||||
/// Defines the runtime options [sometimes] available for a PC.
|
||||
class Options:
|
||||
public Reflection::StructImpl<Options>,
|
||||
public Configurable::DisplayOption<Options>
|
||||
public Configurable::Options::Display<Options>
|
||||
{
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
friend Configurable::Options::Display<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType) :
|
||||
Configurable::DisplayOption<Options>(Configurable::Display::RGB) {}
|
||||
Options(const Configurable::OptionsType) :
|
||||
Configurable::Options::Display<Options>(Configurable::Display::RGB) {}
|
||||
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
|
||||
@@ -170,7 +170,7 @@ public:
|
||||
void preauthorise_stack_read(const uint32_t size, const uint32_t granularity) {
|
||||
const auto &descriptor = segments_.descriptors[InstructionSet::x86::Source::SS];
|
||||
|
||||
const auto trailing_distance = 65536 - registers_.sp();
|
||||
const uint32_t trailing_distance = 65536 - registers_.sp();
|
||||
if(trailing_distance >= size) {
|
||||
descriptor.template authorise<InstructionSet::x86::AccessType::Read, uint16_t>(
|
||||
uint16_t(registers_.sp()),
|
||||
@@ -189,7 +189,7 @@ public:
|
||||
0 // i.e. 65536
|
||||
);
|
||||
|
||||
const auto remainder = size - trailing_distance;
|
||||
const uint32_t remainder = size - trailing_distance;
|
||||
descriptor.template authorise<InstructionSet::x86::AccessType::Read, uint16_t>(
|
||||
0,
|
||||
uint16_t(remainder)
|
||||
@@ -198,10 +198,12 @@ public:
|
||||
}
|
||||
|
||||
void preauthorise_read(const InstructionSet::x86::Source descriptor, const uint16_t offset, const uint32_t size) {
|
||||
segments_.descriptors[descriptor].template authorise<InstructionSet::x86::AccessType::Read, uint16_t>(offset, offset + size);
|
||||
segments_.descriptors[descriptor]
|
||||
.template authorise<InstructionSet::x86::AccessType::Read, uint16_t>(offset, uint16_t(offset + size));
|
||||
}
|
||||
void preauthorise_write(const InstructionSet::x86::Source descriptor, const uint16_t offset, const uint32_t size) {
|
||||
segments_.descriptors[descriptor].template authorise<InstructionSet::x86::AccessType::Write, uint16_t>(offset, offset + size);
|
||||
segments_.descriptors[descriptor]
|
||||
.template authorise<InstructionSet::x86::AccessType::Write, uint16_t>(offset, uint16_t(offset + size));
|
||||
}
|
||||
|
||||
// TODO: perform authorisation checks.
|
||||
|
||||
@@ -186,7 +186,9 @@ public:
|
||||
|
||||
InstructionSet::x86::SegmentRegisterSet<Descriptor> descriptors;
|
||||
|
||||
auto operator <=>(const Segments &rhs) const = default;
|
||||
auto operator ==(const Segments &rhs) const {
|
||||
return descriptors == rhs.descriptors;
|
||||
}
|
||||
|
||||
private:
|
||||
void load_real(const Source segment) {
|
||||
|
||||
@@ -27,7 +27,7 @@ Video::Video() :
|
||||
|
||||
// Show only the centre 80% of the TV frame.
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::CompositeMonochrome);
|
||||
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f));
|
||||
crt_.set_fixed_framing(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f));
|
||||
}
|
||||
|
||||
void Video::run_for(const HalfCycles half_cycles) {
|
||||
|
||||
@@ -114,7 +114,7 @@ public:
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
audio_queue_.lock_flush();
|
||||
}
|
||||
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
@@ -386,14 +386,14 @@ public:
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
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->quick_load = allow_fast_tape_hack_;
|
||||
return options;
|
||||
}
|
||||
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
set_use_automatic_tape_motor_control(options->automatic_tape_motor_control);
|
||||
allow_fast_tape_hack_ = options->quickload;
|
||||
allow_fast_tape_hack_ = options->quick_load;
|
||||
set_use_fast_tape();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,17 +26,17 @@ struct Machine {
|
||||
virtual bool get_tape_is_playing() = 0;
|
||||
|
||||
/// Defines the runtime options available for a ZX80/81.
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::QuickloadOption<Options> {
|
||||
friend Configurable::QuickloadOption<Options>;
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::Options::QuickLoad<Options> {
|
||||
friend Configurable::Options::QuickLoad<Options>;
|
||||
public:
|
||||
bool automatic_tape_motor_control = true;
|
||||
|
||||
Options(Configurable::OptionsType type):
|
||||
Configurable::QuickloadOption<Options>(type == Configurable::OptionsType::UserFriendly),
|
||||
Options(const Configurable::OptionsType type):
|
||||
Configurable::Options::QuickLoad<Options>(type == Configurable::OptionsType::UserFriendly),
|
||||
automatic_tape_motor_control(type == Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
Options() : Options( Configurable::OptionsType::UserFriendly) {}
|
||||
|
||||
friend Reflection::StructImpl<Options>;
|
||||
void declare_fields() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user