mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-04 23:17:01 +00:00
Compare commits
166 Commits
AddressWra
...
2025-10-03
Author | SHA1 | Date | |
---|---|---|---|
|
7229acb34f | ||
|
43cb91760a | ||
|
7bb4d052d1 | ||
|
5885bdf0f8 | ||
|
05042b1859 | ||
|
3ca2f72184 | ||
|
b076450b73 | ||
|
7a90662c06 | ||
|
eb31aaeb7d | ||
|
ebfb215246 | ||
|
eb97e4e518 | ||
|
61d3e65c05 | ||
|
0ac5681d13 | ||
|
1e27c5759b | ||
|
5e71aedc99 | ||
|
d0d8c2316b | ||
|
8f0a5b2191 | ||
|
242b180862 | ||
|
feb4766d7b | ||
|
a224eea077 | ||
|
ecefbc23ae | ||
|
e5e0cbfc53 | ||
|
38781c9395 | ||
|
e70d72b614 | ||
|
fcf648bbb2 | ||
|
a8325b6bce | ||
|
22554a9ba4 | ||
|
02a10ef651 | ||
|
e3ca44f3ca | ||
|
a9abc0dd5f | ||
|
cbcc7c718e | ||
|
4377c79068 | ||
|
514993bc2e | ||
|
c182176134 | ||
|
abd1f10395 | ||
|
f279bebc1a | ||
|
4e3fa5a6ff | ||
|
01d355a247 | ||
|
009f71a186 | ||
|
de5c311d84 | ||
|
eefe34f99e | ||
|
82e3c870b3 | ||
|
d44a1d9761 | ||
|
e6fd54c14b | ||
|
ccb8e90110 | ||
|
a4e66f291a | ||
|
3ab3f34bef | ||
|
99da8c4424 | ||
|
9a1bf1cf74 | ||
|
2256e99157 | ||
|
3a5f8b4987 | ||
|
6de5fcc980 | ||
|
a454d0d4b7 | ||
|
67339754e3 | ||
|
7316fe00ee | ||
|
feb4a7021c | ||
|
5e84b671b6 | ||
|
bcefef62f9 | ||
|
cc953dda34 | ||
|
f9e5b0f0c7 | ||
|
578654411e | ||
|
247e92cfa2 | ||
|
66f605de0f | ||
|
709b0efc9b | ||
|
622679f4c2 | ||
|
fdeb421513 | ||
|
8fe25cde8d | ||
|
fbd71451f1 | ||
|
0d91ce8e6a | ||
|
d71796c88a | ||
|
277748c8f5 | ||
|
8c1358ace9 | ||
|
1254916058 | ||
|
f228bee4b8 | ||
|
8094477b09 | ||
|
32a5bf76cd | ||
|
0375d000e6 | ||
|
141d43d3e5 | ||
|
823f7b1d2e | ||
|
6579f011d0 | ||
|
93f768af9b | ||
|
f8c11bf217 | ||
|
26ccd930c3 | ||
|
82211c7312 | ||
|
2015c154fe | ||
|
ef17d116a8 | ||
|
46fddc44bf | ||
|
0214a77cd7 | ||
|
425ed658f1 | ||
|
a53adb561e | ||
|
3c3c55090a | ||
|
ebc04c6520 | ||
|
8b0e8f5b13 | ||
|
16132a007e | ||
|
b6e41ceea7 | ||
|
7015e46227 | ||
|
cce2607c80 | ||
|
9dd2ec8bda | ||
|
068726e0ab | ||
|
89e86ad9bd | ||
|
2e49bc2044 | ||
|
174c8dafbf | ||
|
90a96293de | ||
|
84877c4fec | ||
|
a7cceb5fa9 | ||
|
ca6359a597 | ||
|
b7c3667be1 | ||
|
b6dea59db3 | ||
|
aa51f13743 | ||
|
f34ec03ff0 | ||
|
1363be59b7 | ||
|
622c24ef24 | ||
|
539b0e49d4 | ||
|
0c42976312 | ||
|
3f6b3a4fa0 | ||
|
67e1773495 | ||
|
a199b64aa0 | ||
|
ebf09aceb2 | ||
|
ca226e4295 | ||
|
9261939f62 | ||
|
0349931953 | ||
|
d612a385d2 | ||
|
ed4f299d55 | ||
|
7cef789d41 | ||
|
66bfb86d42 | ||
|
c4a5bc12ef | ||
|
557631f6ba | ||
|
362ffaff7f | ||
|
fb5ef200fb | ||
|
5e78ac3af5 | ||
|
719a090b34 | ||
|
3af85da6e0 | ||
|
8fd62aa525 | ||
|
40747f51bd | ||
|
f3cef6bd73 | ||
|
eef0ee8180 | ||
|
503e974375 | ||
|
c959f2fee5 | ||
|
7d5e434cba | ||
|
2720bcdf18 | ||
|
c513b7262b | ||
|
57a795df96 | ||
|
6bdd9e4543 | ||
|
ede3def37f | ||
|
87d9022280 | ||
|
ff0ba7d48b | ||
|
b49c47425f | ||
|
3916ba1a42 | ||
|
0b3d22b97c | ||
|
9b8b0f2023 | ||
|
06e0d17be0 | ||
|
239c485f3c | ||
|
5e5fdda0ca | ||
|
4b2dddf3c6 | ||
|
c99ec745ca | ||
|
1ec2e455ec | ||
|
69304737c6 | ||
|
fe91670127 | ||
|
7a59f94f3d | ||
|
4efe3a333d | ||
|
421bf28582 | ||
|
4c49ffe3d1 | ||
|
26b1ef247b | ||
|
3aafba707a | ||
|
ae774e88fa | ||
|
ff56dd53cf |
@@ -19,6 +19,23 @@
|
||||
|
||||
using namespace Analyser::Static::Acorn;
|
||||
|
||||
namespace {
|
||||
bool is_basic(const File &file) {
|
||||
std::size_t pointer = 0;
|
||||
const uint8_t *const data = file.data.data();
|
||||
const std::size_t data_size = file.data.size();
|
||||
while(true) {
|
||||
if(pointer >= data_size-1 || data[pointer] != 0x0d) {
|
||||
return false;
|
||||
}
|
||||
if((data[pointer+1]&0x7f) == 0x7f) break;
|
||||
pointer += data[pointer+3];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
AcornCartridgesFrom(const std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges) {
|
||||
std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> acorn_cartridges;
|
||||
@@ -68,11 +85,16 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
|
||||
TargetPlatform::IntType,
|
||||
bool
|
||||
) {
|
||||
auto target8bit = std::make_unique<ElectronTarget>();
|
||||
auto targetElectron = std::make_unique<ElectronTarget>();
|
||||
auto targetBBC = std::make_unique<BBCMicroTarget>();
|
||||
auto targetArchimedes = std::make_unique<ArchimedesTarget>();
|
||||
int bbc_hits = 0;
|
||||
int electron_hits = 0;
|
||||
bool format_prefers_bbc = false;
|
||||
|
||||
// Copy appropriate cartridges to the 8-bit target.
|
||||
target8bit->media.cartridges = AcornCartridgesFrom(media.cartridges);
|
||||
// Copy appropriate cartridges to the 8-bit targets.
|
||||
targetElectron->media.cartridges = AcornCartridgesFrom(media.cartridges);
|
||||
targetBBC->media.cartridges = AcornCartridgesFrom(media.cartridges);
|
||||
|
||||
// If there are tapes, attempt to get data from the first.
|
||||
if(!media.tapes.empty()) {
|
||||
@@ -80,35 +102,15 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
|
||||
auto serialiser = tape->serialiser();
|
||||
std::vector<File> files = GetFiles(*serialiser);
|
||||
|
||||
// continue if there are any files
|
||||
// Continue only if there are any files.
|
||||
if(!files.empty()) {
|
||||
bool is_basic = true;
|
||||
|
||||
// If a file is execute-only, that means *RUN.
|
||||
if(files.front().flags & File::Flags::ExecuteOnly) {
|
||||
is_basic = false;
|
||||
}
|
||||
|
||||
// Check also for a continuous threading of BASIC lines; if none then this probably isn't BASIC code,
|
||||
// so that's also justification to *RUN.
|
||||
if(is_basic) {
|
||||
std::size_t pointer = 0;
|
||||
uint8_t *const data = &files.front().data[0];
|
||||
const std::size_t data_size = files.front().data.size();
|
||||
while(true) {
|
||||
if(pointer >= data_size-1 || data[pointer] != 0x0d) {
|
||||
is_basic = false;
|
||||
break;
|
||||
}
|
||||
if((data[pointer+1]&0x7f) == 0x7f) break;
|
||||
pointer += data[pointer+3];
|
||||
}
|
||||
}
|
||||
|
||||
// Inspect first file. If it's protected or doesn't look like BASIC
|
||||
// then the loading command is *RUN. Otherwise it's CHAIN"".
|
||||
target8bit->loading_command = is_basic ? "CHAIN\"\"\n" : "*RUN\n";
|
||||
target8bit->media.tapes = media.tapes;
|
||||
targetElectron->loading_command =
|
||||
(files.front().flags & File::Flags::ExecuteOnly) || !is_basic(files.front()) ? "*RUN\n" : "CHAIN\"\"\n";
|
||||
targetElectron->media.tapes = media.tapes;
|
||||
|
||||
// TODO: my BBC Micro doesn't yet support tapes; evaluate here in the future.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,25 +125,50 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
|
||||
// 8-bit options: DFS and Hugo-style ADFS.
|
||||
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.
|
||||
// Use the Pres ADFS if using an ADFS, as it leaves Page at &EOO.
|
||||
target8bit->media.disks = media.disks;
|
||||
target8bit->has_dfs = bool(dfs_catalogue);
|
||||
target8bit->has_pres_adfs = bool(adfs_catalogue);
|
||||
|
||||
// Electron: use the Pres ADFS if using an ADFS, as it leaves Page at &EOO.
|
||||
targetElectron->media.disks = media.disks;
|
||||
targetElectron->has_dfs = bool(dfs_catalogue);
|
||||
targetElectron->has_pres_adfs = bool(adfs_catalogue);
|
||||
|
||||
// BBC: only the 1770 DFS is currently supported, so use that.
|
||||
targetBBC->media.disks = media.disks;
|
||||
targetBBC->has_1770dfs = bool(dfs_catalogue);
|
||||
targetBBC->has_adfs = bool(adfs_catalogue);
|
||||
|
||||
// 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) {
|
||||
target8bit->should_shift_restart = true;
|
||||
targetBBC->should_shift_restart = targetElectron->should_shift_restart = true;
|
||||
} else {
|
||||
target8bit->loading_command = "*CAT\n";
|
||||
// Otherwise: if there's only one BASIC program then chain it.
|
||||
// Failing that, do a *CAT to be communicative.
|
||||
|
||||
const File *sole_basic_file = nullptr;
|
||||
for(const auto &file: dfs_catalogue ? dfs_catalogue->files : adfs_catalogue->files) {
|
||||
if(is_basic(file)) {
|
||||
if(!sole_basic_file) {
|
||||
sole_basic_file = &file;
|
||||
} else {
|
||||
sole_basic_file = nullptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
targetBBC->loading_command = targetElectron->loading_command =
|
||||
sole_basic_file ? "CHAIN \"" + sole_basic_file->name + "\"\n" : "*CAT\n";
|
||||
}
|
||||
|
||||
// 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
|
||||
// on a stock Electron are here, add the AP6 ROM and
|
||||
// some sideways RAM such that the SR commands are useful.
|
||||
// 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) {
|
||||
// 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
|
||||
// on a stock Electron are here, add the AP6 ROM and
|
||||
// some sideways RAM such that the SR commands are useful.
|
||||
for(const auto &command: {
|
||||
"AQRPAGE", "BUILD", "DUMP", "FORMAT", "INSERT", "LANG", "LIST", "LOADROM",
|
||||
"LOCK", "LROMS", "RLOAD", "ROMS", "RSAVE", "SAVEROM", "SRLOAD", "SRPAGE",
|
||||
@@ -149,10 +176,60 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
|
||||
"VERIFY", "ZERO"
|
||||
}) {
|
||||
if(std::search(file.data.begin(), file.data.end(), command, command+strlen(command)) != file.data.end()) {
|
||||
target8bit->has_ap6_rom = true;
|
||||
target8bit->has_sideways_ram = true;
|
||||
targetElectron->has_ap6_rom = true;
|
||||
targetElectron->has_sideways_ram = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for any 'BBC indicators', i.e. direct access to BBC-specific hardware.
|
||||
// Also currently a dense search.
|
||||
|
||||
const auto hits = [&](const std::initializer_list<uint16_t> collection) {
|
||||
int hits = 0;
|
||||
for(const auto address: collection) {
|
||||
const uint8_t sta_address[3] = {
|
||||
0x8d, uint8_t(address & 0xff), uint8_t(address >> 8)
|
||||
};
|
||||
|
||||
if(std::search(
|
||||
file.data.begin(), file.data.end(),
|
||||
std::begin(sta_address), std::end(sta_address)
|
||||
) != file.data.end()) {
|
||||
++hits;
|
||||
}
|
||||
|
||||
// I think I'll want std::ranges::contains_subrange if/when building for C++23.
|
||||
}
|
||||
return hits;
|
||||
};
|
||||
|
||||
bbc_hits += hits({
|
||||
// The video control registers.
|
||||
0xfe20, 0xfe21,
|
||||
|
||||
// The system VIA.
|
||||
0xfe40, 0xfe41, 0xfe42, 0xfe43, 0xfe44, 0xfe45, 0xfe46, 0xfe47,
|
||||
0xfe48, 0xfe49, 0xfe4a, 0xfe4b, 0xfe4c, 0xfe4d, 0xfe4e, 0xfe4f,
|
||||
|
||||
// The user VIA.
|
||||
0xfe60, 0xfe61, 0xfe62, 0xfe63, 0xfe64, 0xfe65, 0xfe66, 0xfe67,
|
||||
0xfe68, 0xfe69, 0xfe6a, 0xfe6b, 0xfe6c, 0xfe6d, 0xfe6e, 0xfe6f,
|
||||
});
|
||||
// BASIC for "MODE7".
|
||||
static constexpr uint8_t mode7[] = {0xeb, 0x37};
|
||||
bbc_hits += std::search(
|
||||
file.data.begin(), file.data.end(),
|
||||
std::begin(mode7), std::end(mode7)
|
||||
) != file.data.end();
|
||||
|
||||
electron_hits += hits({
|
||||
// ULA addresses that aren't also the BBC's CRTC.
|
||||
0xfe03, 0xfe04, 0xfe05,
|
||||
0xfe06, 0xfe07, 0xfe08,
|
||||
0xfe09, 0xfe0a, 0xfe0b,
|
||||
0xfe0c, 0xfe0d, 0xfe0e,
|
||||
0xfe0f,
|
||||
});
|
||||
}
|
||||
} else if(adfs_catalogue) {
|
||||
// Archimedes options, implicitly: ADFS, non-Hugo.
|
||||
@@ -197,28 +274,38 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
|
||||
// Enable the Acorn ADFS if a mass-storage device is attached;
|
||||
// unlike the Pres ADFS it retains SCSI logic.
|
||||
if(!media.mass_storage_devices.empty()) {
|
||||
target8bit->has_pres_adfs = false; // To override a floppy selection, if one was made.
|
||||
target8bit->has_acorn_adfs = true;
|
||||
targetElectron->has_pres_adfs = false; // To override a floppy selection, if one was made.
|
||||
targetElectron->has_acorn_adfs = true;
|
||||
|
||||
// Assume some sort of later-era Acorn work is likely to happen;
|
||||
// so ensure *TYPE, etc are present.
|
||||
target8bit->has_ap6_rom = true;
|
||||
target8bit->has_sideways_ram = true;
|
||||
targetElectron->has_ap6_rom = true;
|
||||
targetElectron->has_sideways_ram = true;
|
||||
|
||||
target8bit->media.mass_storage_devices = media.mass_storage_devices;
|
||||
targetElectron->media.mass_storage_devices = media.mass_storage_devices;
|
||||
|
||||
// Check for a boot option.
|
||||
const auto sector = target8bit->media.mass_storage_devices.front()->get_block(1);
|
||||
const auto sector = targetElectron->media.mass_storage_devices.front()->get_block(1);
|
||||
if(sector[0xfd]) {
|
||||
target8bit->should_shift_restart = true;
|
||||
targetElectron->should_shift_restart = true;
|
||||
} else {
|
||||
target8bit->loading_command = "*CAT\n";
|
||||
targetElectron->loading_command = "*CAT\n";
|
||||
}
|
||||
}
|
||||
|
||||
TargetList targets;
|
||||
if(!target8bit->media.empty()) {
|
||||
targets.push_back(std::move(target8bit));
|
||||
if(!targetElectron->media.empty() && !targetBBC->media.empty()) {
|
||||
if(bbc_hits > electron_hits || (bbc_hits == electron_hits && format_prefers_bbc)) {
|
||||
targets.push_back(std::move(targetBBC));
|
||||
} else {
|
||||
targets.push_back(std::move(targetElectron));
|
||||
}
|
||||
} else {
|
||||
if(!targetElectron->media.empty()) {
|
||||
targets.push_back(std::move(targetElectron));
|
||||
} else if(!targetBBC->media.empty()) {
|
||||
targets.push_back(std::move(targetBBC));
|
||||
}
|
||||
}
|
||||
if(!targetArchimedes->media.empty()) {
|
||||
targets.push_back(std::move(targetArchimedes));
|
||||
|
@@ -38,12 +38,21 @@ private:
|
||||
|
||||
struct BBCMicroTarget: public ::Analyser::Static::Target, public Reflection::StructImpl<BBCMicroTarget> {
|
||||
std::string loading_command;
|
||||
bool should_shift_restart = false;
|
||||
|
||||
bool has_1770dfs = false;
|
||||
bool has_adfs = false;
|
||||
bool has_sideways_ram = true;
|
||||
|
||||
BBCMicroTarget() : Analyser::Static::Target(Machine::BBCMicro) {}
|
||||
|
||||
private:
|
||||
friend Reflection::StructImpl<BBCMicroTarget>;
|
||||
void declare_fields() {}
|
||||
void declare_fields() {
|
||||
DeclareField(has_1770dfs);
|
||||
DeclareField(has_adfs);
|
||||
DeclareField(has_sideways_ram);
|
||||
}
|
||||
};
|
||||
|
||||
struct ArchimedesTarget: public ::Analyser::Static::Target, public Reflection::StructImpl<ArchimedesTarget> {
|
||||
|
@@ -114,6 +114,7 @@ uint8_t WD1770::read(const int address) {
|
||||
update_status([] (Status &status) {
|
||||
status.data_request = false;
|
||||
});
|
||||
// Logger::info().append("Returned data %02x; [drq:%d]", data_, status_.data_request);
|
||||
return data_;
|
||||
}
|
||||
}
|
||||
@@ -134,12 +135,13 @@ void WD1770::run_for(const Cycles cycles) {
|
||||
|
||||
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 RESUME_WAIT(mask) interesting_event_mask_ = int(mask); return;
|
||||
#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; \
|
||||
RESUME_WAIT(Event::Token); \
|
||||
}
|
||||
#define BEGIN_SECTION() switch(resume_point_) { default:
|
||||
#define END_SECTION() (void)0; }
|
||||
|
@@ -23,7 +23,7 @@ template <typename T> void MOS6522<T>::access(const int address) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xf:
|
||||
// case 0xf:
|
||||
case 0x1:
|
||||
// In both handshake and pulse modes, CA2 goes low on any read or write of Port A.
|
||||
if(handshake_modes_[0] != HandshakeMode::None) {
|
||||
@@ -57,14 +57,17 @@ template <typename T> void MOS6522<T>::write(int address, const uint8_t value) {
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
bus_handler_.template set_port_output<Port::A>(value, registers_.data_direction[0]);
|
||||
|
||||
if(handshake_modes_[1] != HandshakeMode::None) {
|
||||
set_control_line_output<Port::A, Line::Two>(LineState::Off);
|
||||
}
|
||||
// Avoid handshaking if this was via address 0xf.
|
||||
if(address == 0x1) {
|
||||
if(handshake_modes_[1] != HandshakeMode::None) {
|
||||
set_control_line_output<Port::A, Line::Two>(LineState::Off);
|
||||
}
|
||||
|
||||
registers_.interrupt_flags &= ~(
|
||||
InterruptFlag::CA1ActiveEdge |
|
||||
((registers_.peripheral_control&0x02) ? 0 : InterruptFlag::CB2ActiveEdge)
|
||||
);
|
||||
registers_.interrupt_flags &= ~(
|
||||
InterruptFlag::CA1ActiveEdge |
|
||||
((registers_.peripheral_control&0x02) ? 0 : InterruptFlag::CB2ActiveEdge)
|
||||
);
|
||||
}
|
||||
reevaluate_interrupts();
|
||||
break;
|
||||
|
||||
@@ -196,9 +199,10 @@ template <typename T> uint8_t MOS6522<T>::read(int address) {
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | InterruptFlag::CB2ActiveEdge);
|
||||
reevaluate_interrupts();
|
||||
return get_port_input<Port::B>(registers_.data_direction[1], registers_.output[1], registers_.auxiliary_control & 0x80);
|
||||
case 0xf:
|
||||
case 0x1: // Read Port A ('IRA').
|
||||
case 0x1: // Read Port A ('IRA') [with handshaking].
|
||||
registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge);
|
||||
[[fallthrough]];
|
||||
case 0xf: // Read Port A ('IRA') [without handshaking].
|
||||
reevaluate_interrupts();
|
||||
return get_port_input<Port::A>(registers_.data_direction[0], registers_.output[0], 0);
|
||||
|
||||
@@ -246,8 +250,10 @@ uint8_t MOS6522<T>::get_port_input(
|
||||
) {
|
||||
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
|
||||
const uint8_t input = bus_handler_.template get_port_input<port>();
|
||||
output = (output & ~timer_mask) | (registers_.timer_port_b_output & timer_mask);
|
||||
return (input & ~output_mask) | (output & output_mask);
|
||||
|
||||
// Force any timer-adjusted PB7 to be visible even if the pin is set as input.
|
||||
output = (input & ~output_mask) | (output & output_mask);
|
||||
return (output & ~timer_mask) | (registers_.timer_port_b_output & timer_mask);
|
||||
}
|
||||
|
||||
template <typename T> T &MOS6522<T>::bus_handler() {
|
||||
|
@@ -9,23 +9,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "ClockReceiver/ClockReceiver.hpp"
|
||||
#include "Numeric/SizedInt.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
//
|
||||
// WARNING: code is in flux. I'm attempting to use hoglet's FPGA implementation at
|
||||
// https://github.com/hoglet67/BeebFpga/blob/master/src/common/mc6845.vhd as an authoritative guide to proper behaviour,
|
||||
// having found his Electron ULA to be excellent. This is starting by mapping various bits of internal state here
|
||||
// to hoglet's equivalents; cf. comments.
|
||||
//
|
||||
|
||||
namespace Motorola::CRTC {
|
||||
|
||||
using RefreshAddress = Numeric::SizedInt<14>;
|
||||
using LineAddress = Numeric::SizedInt<5>;
|
||||
|
||||
using SyncCounter = Numeric::SizedInt<4>;
|
||||
using CharacterAddress = Numeric::SizedInt<8>;
|
||||
using RowAddress = Numeric::SizedInt<7>;
|
||||
|
||||
struct BusState {
|
||||
bool display_enable = false;
|
||||
bool hsync = false;
|
||||
bool vsync = false;
|
||||
bool hsync = false; // hs
|
||||
bool vsync = false; // vs
|
||||
bool cursor = false;
|
||||
uint16_t refresh_address = 0;
|
||||
uint16_t row_address = 0;
|
||||
RefreshAddress refresh;
|
||||
LineAddress line;
|
||||
|
||||
// Not strictly part of the bus state; provided because the partition between 6845 and bus handler
|
||||
// doesn't quite hold up in some emulated systems where the two are integrated and share more state.
|
||||
int field_count = 0;
|
||||
Numeric::SizedInt<5> field_count = 0; // field_counter
|
||||
};
|
||||
|
||||
class BusHandler {
|
||||
@@ -39,14 +54,8 @@ enum class Personality {
|
||||
UM6845R, // Type 1 in CPC parlance. Status register, fixed-length VSYNC.
|
||||
MC6845, // Type 2. No status register, fixed-length VSYNC, no zero-length HSYNC.
|
||||
AMS40226, // Type 3. Status is get register, fixed-length VSYNC, no zero-length HSYNC.
|
||||
|
||||
EGA, // Extended EGA-style CRTC; uses 16-bit addressing throughout.
|
||||
};
|
||||
|
||||
constexpr bool is_egavga(const Personality p) {
|
||||
return p >= Personality::EGA;
|
||||
}
|
||||
|
||||
// https://www.pcjs.org/blog/2018/03/20/ advises that "the behavior of bits 5 and 6 [of register 10, the cursor start
|
||||
// register is really card specific".
|
||||
//
|
||||
@@ -54,10 +63,10 @@ constexpr bool is_egavga(const Personality p) {
|
||||
enum class CursorType {
|
||||
/// No cursor signal is generated.
|
||||
None,
|
||||
/// Built-in 6845 style: 00 => no blinking; 01 => no cursor; 10 => slow blink; 11 => fast blink
|
||||
Native,
|
||||
/// MDA style: 00 => symmetric blinking; 01 or 10 => no blinking; 11 => short on, long off.
|
||||
MDA,
|
||||
/// EGA style: ignore the bits completely.
|
||||
EGA,
|
||||
};
|
||||
|
||||
// TODO UM6845R and R12/R13; see http://www.cpcwiki.eu/index.php/CRTC#CRTC_Differences
|
||||
@@ -90,38 +99,26 @@ public:
|
||||
}
|
||||
|
||||
void set_register(const uint8_t value) {
|
||||
static constexpr bool is_ega = is_egavga(personality);
|
||||
|
||||
const auto load_low = [value](uint16_t &target) {
|
||||
target = (target & 0xff00) | value;
|
||||
};
|
||||
const auto load_high = [value](uint16_t &target) {
|
||||
static constexpr uint8_t mask = RefreshMask >> 8;
|
||||
target = uint16_t((target & 0x00ff) | ((value & mask) << 8));
|
||||
};
|
||||
|
||||
switch(selected_register_) {
|
||||
case 0: layout_.horizontal.total = value; break;
|
||||
case 1: layout_.horizontal.displayed = value; break;
|
||||
case 2: layout_.horizontal.start_sync = value; break;
|
||||
case 3:
|
||||
layout_.horizontal.sync_width = value & 0xf;
|
||||
layout_.horizontal.sync_width = value;
|
||||
layout_.vertical.sync_lines = value >> 4;
|
||||
// TODO: vertical sync lines:
|
||||
// "(0 means 16 on some CRTC. Not present on all CRTCs, fixed to 16 lines on these)"
|
||||
break;
|
||||
case 4: layout_.vertical.total = value & 0x7f; break;
|
||||
case 5: layout_.vertical.adjust = value & 0x1f; break;
|
||||
case 6: layout_.vertical.displayed = value & 0x7f; break;
|
||||
case 7: layout_.vertical.start_sync = value & 0x7f; break;
|
||||
case 4: layout_.vertical.total = value; break;
|
||||
case 5: layout_.vertical.adjust = value; break;
|
||||
case 6: layout_.vertical.displayed = value; break;
|
||||
case 7: layout_.vertical.start_sync = value; break;
|
||||
case 8:
|
||||
// TODO: an error elsewhere appears to cause modes other than InterlaceMode::Off never to hit
|
||||
// vertical sync.
|
||||
// switch(value & 3) {
|
||||
// default: layout_.interlace_mode_ = InterlaceMode::Off; break;
|
||||
// case 0b01: layout_.interlace_mode_ = InterlaceMode::InterlaceSync; break;
|
||||
// case 0b11: layout_.interlace_mode_ = InterlaceMode::InterlaceSyncAndVideo; break;
|
||||
// }
|
||||
switch(value & 3) {
|
||||
default: layout_.interlace_mode_ = InterlaceMode::Off; break;
|
||||
case 0b01: layout_.interlace_mode_ = InterlaceMode::Sync; break;
|
||||
case 0b11: layout_.interlace_mode_ = InterlaceMode::SyncAndVideo; break;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -132,20 +129,23 @@ public:
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 9: layout_.vertical.end_row = value & 0x1f; break;
|
||||
case 9: layout_.vertical.end_line = value; break;
|
||||
case 10:
|
||||
layout_.vertical.start_cursor = value & 0x1f;
|
||||
layout_.cursor_flags = (value >> 5) & 3;
|
||||
layout_.vertical.start_cursor = value;
|
||||
layout_.cursor_flags = value >> 5;
|
||||
update_cursor_mask();
|
||||
break;
|
||||
case 11:
|
||||
layout_.vertical.end_cursor = value & 0x1f;
|
||||
layout_.vertical.end_cursor = value;
|
||||
break;
|
||||
case 12: load_high(layout_.start_address); break;
|
||||
case 13: load_low(layout_.start_address); break;
|
||||
case 14: load_high(layout_.cursor_address); break;
|
||||
case 15: load_low(layout_.cursor_address); break;
|
||||
case 12: layout_.start_address.template load<8>(value); break;
|
||||
case 13: layout_.start_address.template load<0>(value); break;
|
||||
case 14: layout_.cursor_address.template load<8>(value); break;
|
||||
case 15: layout_.cursor_address.template load<0>(value); break;
|
||||
}
|
||||
|
||||
// Take redundant copies of all registers, limited to their actual bit sizes,
|
||||
// to proffer up if the registers are read.
|
||||
static constexpr uint8_t masks[] = {
|
||||
0xff, // Horizontal total.
|
||||
0xff, // Horizontal display end.
|
||||
@@ -154,11 +154,11 @@ public:
|
||||
// EGA: b0–b4: end of horizontal blank;
|
||||
// b5–b6: "Number of character clocks to delay start of display after Horizontal Total has been reached."
|
||||
|
||||
is_ega ? 0xff : 0x7f, // Start horizontal retrace.
|
||||
0x7f, // Start horizontal retrace.
|
||||
0x1f, 0x7f, 0x7f,
|
||||
0xff, 0x1f, 0x7f, 0x1f,
|
||||
uint8_t(RefreshMask >> 8), uint8_t(RefreshMask),
|
||||
uint8_t(RefreshMask >> 8), uint8_t(RefreshMask),
|
||||
uint8_t(RefreshAddress::Mask >> 8), uint8_t(RefreshAddress::Mask),
|
||||
uint8_t(RefreshAddress::Mask >> 8), uint8_t(RefreshAddress::Mask),
|
||||
};
|
||||
|
||||
if(selected_register_ < 16) {
|
||||
@@ -170,8 +170,8 @@ public:
|
||||
}
|
||||
|
||||
void trigger_light_pen() {
|
||||
registers_[17] = bus_state_.refresh_address & 0xff;
|
||||
registers_[16] = bus_state_.refresh_address >> 8;
|
||||
registers_[17] = bus_state_.refresh.get() & 0xff;
|
||||
registers_[16] = bus_state_.refresh.get() >> 8;
|
||||
status_ |= 0x40;
|
||||
}
|
||||
|
||||
@@ -182,180 +182,291 @@ public:
|
||||
// ordered so that whatever assignments result don't affect any subsequent conditionals
|
||||
|
||||
|
||||
// Do bus work.
|
||||
bus_state_.cursor = is_cursor_line_ &&
|
||||
bus_state_.refresh_address == layout_.cursor_address;
|
||||
bus_state_.display_enable = character_is_visible_ && line_is_visible_;
|
||||
bus_handler_.perform_bus_cycle(bus_state_);
|
||||
//
|
||||
// External bus activity.
|
||||
//
|
||||
bus_state_.line = line_is_interlaced_ ? (line_ & LineAddress::IntT(~1)) | (odd_field_ ? 1 : 0) : line_;
|
||||
bus_state_.display_enable = character_is_visible_ && row_is_visible_;
|
||||
bus_state_.cursor = (cursor_mask_ && is_cursor_line_ && bus_state_.refresh == layout_.cursor_address)
|
||||
&& bus_state_.display_enable;
|
||||
|
||||
bus_handler_.perform_bus_cycle(bus_state_);
|
||||
|
||||
bus_state_.refresh = refresh_; // Deliberate: do this after bus activity.
|
||||
// TODO: is this a hack?
|
||||
|
||||
|
||||
//
|
||||
// Shared, stateless signals.
|
||||
// Shared signals.
|
||||
//
|
||||
const bool character_total_hit = character_counter_ == layout_.horizontal.total;
|
||||
const uint8_t lines_per_row =
|
||||
layout_.interlace_mode_ == InterlaceMode::InterlaceSyncAndVideo ?
|
||||
layout_.vertical.end_row & ~1 : layout_.vertical.end_row;
|
||||
const bool row_end_hit = bus_state_.row_address == lines_per_row && !is_in_adjustment_period_;
|
||||
const bool was_eof = eof_latched_;
|
||||
const bool character_total_hit = character_counter_ == layout_.horizontal.total; // r00_h_total_hit
|
||||
const auto lines_per_row =
|
||||
layout_.interlace_mode_ == InterlaceMode::SyncAndVideo ?
|
||||
layout_.vertical.end_line & LineAddress::IntT(~1) : layout_.vertical.end_line; // max_scanline
|
||||
const bool line_end_hit = line_ == lines_per_row && !is_in_adjustment_period_; // max_scanline_hit
|
||||
const bool new_frame =
|
||||
character_total_hit && was_eof &&
|
||||
character_total_hit && eof_latched_ &&
|
||||
(
|
||||
layout_.interlace_mode_ == InterlaceMode::Off ||
|
||||
!odd_field_
|
||||
);
|
||||
!bus_state_.field_count.bit<0>() ||
|
||||
extra_line_
|
||||
); // new_frame
|
||||
|
||||
//
|
||||
// Horizontal.
|
||||
//
|
||||
|
||||
// Update horizontal sync.
|
||||
if(bus_state_.hsync) {
|
||||
++hsync_counter_;
|
||||
bus_state_.hsync = hsync_counter_ != layout_.horizontal.sync_width;
|
||||
}
|
||||
if(character_counter_ == layout_.horizontal.start_sync) {
|
||||
hsync_counter_ = 0;
|
||||
bus_state_.hsync = true;
|
||||
}
|
||||
|
||||
// Check for end-of-line.
|
||||
character_reset_history_ <<= 1;
|
||||
if(character_total_hit) {
|
||||
character_counter_ = 0;
|
||||
character_is_visible_ = true;
|
||||
character_reset_history_ |= 1;
|
||||
} else {
|
||||
character_counter_++;
|
||||
}
|
||||
|
||||
// Check for end of visible characters.
|
||||
if(character_counter_ == layout_.horizontal.displayed) {
|
||||
character_is_visible_ = false;
|
||||
}
|
||||
|
||||
//
|
||||
// End-of-frame.
|
||||
//
|
||||
|
||||
if(character_total_hit) {
|
||||
if(was_eof) {
|
||||
eof_latched_ = eom_latched_ = is_in_adjustment_period_ = false;
|
||||
adjustment_counter_ = 0;
|
||||
} else if(is_in_adjustment_period_) {
|
||||
adjustment_counter_ = (adjustment_counter_ + 1) & 31;
|
||||
}
|
||||
}
|
||||
|
||||
if(character_reset_history_ & 2) {
|
||||
eom_latched_ |= row_end_hit && row_counter_ == layout_.vertical.total;
|
||||
}
|
||||
|
||||
if(character_reset_history_ & 4 && eom_latched_) {
|
||||
// TODO: I don't believe the "add 1 for interlaced" test here is accurate;
|
||||
// others represent the extra scanline as additional state, presumably because
|
||||
// adjust total might be reprogrammed at any time.
|
||||
const auto adjust_length =
|
||||
layout_.vertical.adjust + (layout_.interlace_mode_ != InterlaceMode::Off && odd_field_ ? 1 : 0);
|
||||
is_in_adjustment_period_ |= adjustment_counter_ != adjust_length;
|
||||
eof_latched_ |= adjustment_counter_ == adjust_length;
|
||||
}
|
||||
|
||||
//
|
||||
// Vertical.
|
||||
//
|
||||
|
||||
// Sync.
|
||||
const bool vsync_horizontal =
|
||||
(!odd_field_ && !character_counter_) ||
|
||||
(odd_field_ && character_counter_ == (layout_.horizontal.total >> 1));
|
||||
if(vsync_horizontal) {
|
||||
if((row_counter_ == layout_.vertical.start_sync && !bus_state_.row_address) || bus_state_.vsync) {
|
||||
bus_state_.vsync = true;
|
||||
vsync_counter_ = (vsync_counter_ + 1) & 0xf;
|
||||
} else {
|
||||
vsync_counter_ = -1;//0; // TODO: this ensures the first time the condition above is met,
|
||||
// vsync_counter starts at 0. It's a hack though.
|
||||
}
|
||||
|
||||
if(vsync_counter_ == layout_.vertical.sync_lines) {
|
||||
bus_state_.vsync = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Row address.
|
||||
if(character_total_hit) {
|
||||
if(was_eof) {
|
||||
bus_state_.row_address = 0;
|
||||
eof_latched_ = eom_latched_ = false;
|
||||
} else if(row_end_hit) {
|
||||
bus_state_.row_address = 0;
|
||||
} else if(layout_.interlace_mode_ == InterlaceMode::InterlaceSyncAndVideo) {
|
||||
bus_state_.row_address = (bus_state_.row_address + 2) & ~1 & 31;
|
||||
} else {
|
||||
bus_state_.row_address = (bus_state_.row_address + 1) & 31;
|
||||
}
|
||||
}
|
||||
|
||||
// Row counter.
|
||||
row_counter_ = next_row_counter_;
|
||||
if(new_frame) {
|
||||
next_row_counter_ = 0;
|
||||
is_first_scanline_ = true;
|
||||
} else {
|
||||
next_row_counter_ = row_end_hit && character_total_hit ?
|
||||
(next_row_counter_ + 1) : next_row_counter_;
|
||||
is_first_scanline_ &= !row_end_hit;
|
||||
}
|
||||
|
||||
// Vertical display enable.
|
||||
if(is_first_scanline_) {
|
||||
line_is_visible_ = true;
|
||||
odd_field_ = bus_state_.field_count & 1;
|
||||
} else if(line_is_visible_ && row_counter_ == layout_.vertical.displayed) {
|
||||
line_is_visible_ = false;
|
||||
++bus_state_.field_count;
|
||||
}
|
||||
|
||||
|
||||
// Cursor.
|
||||
if constexpr (cursor_type != CursorType::None) {
|
||||
// Check for cursor enable.
|
||||
is_cursor_line_ |= bus_state_.row_address == layout_.vertical.start_cursor;
|
||||
is_cursor_line_ &= bus_state_.row_address != layout_.vertical.end_cursor;
|
||||
|
||||
switch(cursor_type) {
|
||||
// MDA-style blinking.
|
||||
// https://retrocomputing.stackexchange.com/questions/27803/what-are-the-blinking-rates-of-the-caret-and-of-blinking-text-on-pc-graphics-car
|
||||
// gives an 8/8 pattern for regular blinking though mode 11 is then just a guess.
|
||||
case CursorType::MDA:
|
||||
switch(layout_.cursor_flags) {
|
||||
case 0b11: is_cursor_line_ &= (bus_state_.field_count & 8) < 3; break;
|
||||
case 0b00: is_cursor_line_ &= bool(bus_state_.field_count & 8); break;
|
||||
case 0b01: is_cursor_line_ = false; break;
|
||||
case 0b10: is_cursor_line_ = true; break;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Addressing.
|
||||
//
|
||||
|
||||
// Start-of-line address: seeded with the programmed display start address upon a new frame;
|
||||
// otherwise copied from the refresh address at the end of each line of characters.
|
||||
const auto initial_line_address = line_address_;
|
||||
if(new_frame) {
|
||||
bus_state_.refresh_address = layout_.start_address;
|
||||
line_address_ = layout_.start_address;
|
||||
} else if(character_counter_ == layout_.horizontal.displayed && line_end_hit) {
|
||||
line_address_ = refresh_;
|
||||
}
|
||||
|
||||
// Refresh address: seeded with the programmed display start address upon a new frame;
|
||||
// otherwise copied from the start-of-line address is a new line is about to start;
|
||||
// otherwise incremented across the line.
|
||||
if(new_frame) {
|
||||
refresh_ = layout_.start_address;
|
||||
} else if(character_total_hit) {
|
||||
bus_state_.refresh_address = line_address_;
|
||||
refresh_ = initial_line_address;
|
||||
} else {
|
||||
bus_state_.refresh_address = (bus_state_.refresh_address + 1) & RefreshMask;
|
||||
++refresh_;
|
||||
}
|
||||
|
||||
//
|
||||
// Per hoglet: b0 does not vary within a line even if you switch in/out of interlaced mode.
|
||||
// He reproduces the same with extra state, which probably doesn't exist on the real device.
|
||||
// This implementation follows his lead.
|
||||
//
|
||||
if(character_total_hit) {
|
||||
line_is_interlaced_ = layout_.interlace_mode_ == InterlaceMode::SyncAndVideo;
|
||||
}
|
||||
|
||||
//
|
||||
// Sync.
|
||||
//
|
||||
|
||||
// Vertical sync.
|
||||
//
|
||||
// Counter:
|
||||
// Sync width of 0 => 16 lines of sync.
|
||||
// Triggered by the row counter becoming equal to the sync start position, regardless of when.
|
||||
// Subsequently increments at the start of each line.
|
||||
const bool hit_vsync = row_counter_ == layout_.vertical.start_sync; // vs_hit
|
||||
const bool is_vsync_rising_edge = hit_vsync && !hit_vsync_last_;
|
||||
hit_vsync_last_ = hit_vsync;
|
||||
|
||||
// Select odd or even sync depending on the field.
|
||||
// (Noted: the reverse-odd-test is intentional)
|
||||
bus_state_.vsync = (layout_.interlace_mode_ != InterlaceMode::Off && !odd_field_) ?
|
||||
vsync_odd_ : vsync_even_;
|
||||
|
||||
// Odd sync copies even sync, but half a line later.
|
||||
if(character_counter_ == layout_.horizontal.total >> 1) {
|
||||
vsync_odd_ = vsync_even_;
|
||||
}
|
||||
|
||||
// Even sync begins on the rising edge of vsync, then continues until the counter hits its proper
|
||||
// target, one cycle after reset of the horizontal counter.
|
||||
if(is_vsync_rising_edge) {
|
||||
vsync_even_ = true;
|
||||
} else if(vsync_counter_ == layout_.vertical.sync_lines && character_reset_history_.bit<0>()) {
|
||||
vsync_even_ = false;
|
||||
}
|
||||
|
||||
// The vsync counter is zeroed by the rising edge of sync but subsequently increments immediately
|
||||
// upon reset of the horizontal counter.
|
||||
if(is_vsync_rising_edge) {
|
||||
vsync_counter_ = 0;
|
||||
} else if(character_total_hit) {
|
||||
++vsync_counter_;
|
||||
}
|
||||
|
||||
// Horizontal sync.
|
||||
//
|
||||
// A sync width of 0 should mean that no sync is observed.
|
||||
// Hitting the start sync condition while sync is already ongoing should have no effect.
|
||||
if(bus_state_.hsync) {
|
||||
++hsync_counter_;
|
||||
} else {
|
||||
hsync_counter_ = 0;
|
||||
}
|
||||
if(hsync_counter_ == layout_.horizontal.sync_width) {
|
||||
bus_state_.hsync = false;
|
||||
} else if(character_counter_ == layout_.horizontal.start_sync) {
|
||||
bus_state_.hsync = true;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Horizontal.
|
||||
//
|
||||
|
||||
// Check for visible characters; visibility starts in the first column and continues
|
||||
if(!character_counter_) {
|
||||
character_is_visible_ = true;
|
||||
}
|
||||
if(character_counter_ == layout_.horizontal.displayed || character_total_hit) {
|
||||
character_is_visible_ = false;
|
||||
}
|
||||
|
||||
// Check for end-of-line.
|
||||
//
|
||||
// character_reset_history_ is used because some events are defined to occur one or two
|
||||
// cycles after end-of-line regardless of whether an additional end of line is hit in
|
||||
// the interim.
|
||||
if(character_total_hit) {
|
||||
character_counter_ = 0;
|
||||
} else {
|
||||
++character_counter_;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Vertical.
|
||||
//
|
||||
|
||||
// Update line counter (which also counts the vertical adjust period).
|
||||
//
|
||||
// Counts in steps of 2 only if & 3) mode is InterlaceMode::SyncAndVideo and this is
|
||||
// not the adjustment period. Otherwise counts in steps of 1.
|
||||
if(new_frame) {
|
||||
line_ = 0;
|
||||
} else if(character_total_hit) {
|
||||
line_ = next_line_;
|
||||
}
|
||||
|
||||
if(line_end_hit) {
|
||||
next_line_ = 0;
|
||||
} else if(is_in_adjustment_period_ || layout_.interlace_mode_ != InterlaceMode::SyncAndVideo) {
|
||||
next_line_ = line_ + 1;
|
||||
} else {
|
||||
next_line_ = (line_ + 2) & LineAddress::IntT(~1);
|
||||
}
|
||||
|
||||
// Update row counter.
|
||||
//
|
||||
// Very straightforward: tests at end of line whether row end has also been hit. If so, increments.
|
||||
row_counter_ = next_row_counter_;
|
||||
if(new_frame) {
|
||||
next_row_counter_ = 0;
|
||||
} else if(character_total_hit && line_end_hit) {
|
||||
next_row_counter_ = row_counter_ + 1;
|
||||
}
|
||||
|
||||
// Vertical display enable.
|
||||
if(is_first_scanline_) {
|
||||
row_is_visible_ = true;
|
||||
odd_field_ = bus_state_.field_count.bit<0>();
|
||||
} else if(row_is_visible_ && row_counter_ == layout_.vertical.displayed) {
|
||||
row_is_visible_ = false;
|
||||
++bus_state_.field_count;
|
||||
update_cursor_mask();
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// End-of-frame.
|
||||
//
|
||||
|
||||
if(new_frame) {
|
||||
is_in_adjustment_period_ = false;
|
||||
} else if(character_total_hit && eom_latched_ && will_adjust_) {
|
||||
is_in_adjustment_period_ = true;
|
||||
}
|
||||
|
||||
if(new_frame) {
|
||||
line_address_ = layout_.start_address;
|
||||
} else if(character_counter_ == layout_.horizontal.displayed && row_end_hit) {
|
||||
line_address_ = bus_state_.refresh_address;
|
||||
is_first_scanline_ = true;
|
||||
} else if(character_total_hit) {
|
||||
is_first_scanline_ = false;
|
||||
}
|
||||
|
||||
// The extra-line flag holds true for a single line if one is needed to complete
|
||||
// an odd interlaced field.
|
||||
if(
|
||||
character_total_hit &&
|
||||
eof_latched_ &&
|
||||
layout_.interlace_mode_ != InterlaceMode::Off &&
|
||||
bus_state_.field_count.bit<0>() &&
|
||||
!extra_line_
|
||||
) {
|
||||
extra_line_ = true;
|
||||
} else if(character_total_hit) {
|
||||
extra_line_ = false;
|
||||
}
|
||||
|
||||
// EOF (end of field) marks the end of the regular set of scans, including the adjustment area.
|
||||
// It doesn't include the extra line added during odd interlaced fields.
|
||||
if(new_frame) {
|
||||
eof_latched_ = false;
|
||||
} else if(eom_latched_ && !will_adjust_ && character_reset_history_.bit<2>()) {
|
||||
eof_latched_ = true;
|
||||
}
|
||||
|
||||
// Will-adjust indicates whether an adjustment area is upcoming; if so then it occurs after EOM.
|
||||
if(new_frame) {
|
||||
will_adjust_ = false;
|
||||
} else if(character_reset_history_.bit<1>() && eom_latched_) {
|
||||
if(next_line_ == layout_.vertical.adjust) {
|
||||
will_adjust_ = false;
|
||||
} else {
|
||||
will_adjust_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
// EOM (end of main) marks the end of the visible set of rows, prior to any adjustment area.
|
||||
// It is set one cycle after the most-recent start of line.
|
||||
if(new_frame) {
|
||||
eom_latched_ = false;
|
||||
} else if(character_reset_history_.bit<0>() && line_end_hit && row_counter_ == layout_.vertical.total) {
|
||||
eom_latched_ = true;
|
||||
}
|
||||
|
||||
//
|
||||
// Cursor
|
||||
//
|
||||
cursor_history_ <<= 1;
|
||||
if constexpr (cursor_type != CursorType::None) {
|
||||
if(character_total_hit) {
|
||||
// This is clearly a nonsense test; there's absolutely no reason a real 6845 would do anything
|
||||
// other than equality comparisons, to maintain internal state.
|
||||
//
|
||||
// ... that said, I have been unable to reconcile:
|
||||
//
|
||||
// 1. the PCjs results on real MC6845Ps that show wraparound cursors
|
||||
// Cf. https://www.pcjs.org/blog/2018/03/20/ ; and
|
||||
// 2. the expectations of the BBC Micro (which sets an out-of-range stop line for its cursor
|
||||
// right at initial boot) and various pieces of its software (including but
|
||||
// not limited to Arcadians, which uses in-range numbers but has start > end and expects
|
||||
// the cursor correspondingly to be hidden).
|
||||
//
|
||||
// I also note that the two BBC FPGA implementations I glanced at, hoglet's and Mister's, use
|
||||
// fictional range comparisons.
|
||||
//
|
||||
// But, on the other hand, Tom Seddon remarks at https://github.com/tom-seddon/6845-tests that
|
||||
// "Looks like the cursor switches on when cursor is off and raster matches R10, and switches
|
||||
// off when cursor is on and raster matches R11."
|
||||
//
|
||||
// (but also seems to use a range test in his software implementation?)
|
||||
is_cursor_line_ =
|
||||
line_ >= layout_.vertical.start_cursor &&
|
||||
line_ <= layout_.vertical.end_cursor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Event history.
|
||||
//
|
||||
|
||||
// Somewhat of a fiction, this keeps a track of recent character resets because
|
||||
// some events are keyed on 1 cycle after last reset, 2 cycles after last reset, etc.
|
||||
character_reset_history_ <<= 1;
|
||||
character_reset_history_ |= character_total_hit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,78 +475,124 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint16_t RefreshMask = (personality >= Personality::EGA) ? 0xffff : 0x3fff;
|
||||
|
||||
BusHandlerT &bus_handler_;
|
||||
BusState bus_state_;
|
||||
|
||||
enum class InterlaceMode {
|
||||
/// No interlacing.
|
||||
Off,
|
||||
InterlaceSync,
|
||||
InterlaceSyncAndVideo,
|
||||
/// Provide interlaced sync, but just scan out the exact same display for each field.
|
||||
Sync,
|
||||
/// 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 {
|
||||
struct {
|
||||
uint8_t total;
|
||||
uint8_t displayed;
|
||||
uint8_t start_sync;
|
||||
uint8_t sync_width;
|
||||
CharacterAddress total; // r00_h_total
|
||||
CharacterAddress displayed; // r01_h_displayed
|
||||
CharacterAddress start_sync; // r02_h_sync_pos
|
||||
SyncCounter sync_width; // r03_h_sync_width
|
||||
} horizontal;
|
||||
|
||||
struct {
|
||||
uint8_t total;
|
||||
uint8_t displayed;
|
||||
uint8_t start_sync;
|
||||
uint8_t sync_lines;
|
||||
uint8_t adjust;
|
||||
RowAddress total; // r04_v_total
|
||||
RowAddress displayed; // r06_v_displayed
|
||||
RowAddress start_sync; // r07_v_sync_pos
|
||||
SyncCounter sync_lines; // r03_v_sync_width
|
||||
LineAddress adjust; // r05_v_total_adj
|
||||
|
||||
uint8_t end_row;
|
||||
uint8_t start_cursor;
|
||||
uint8_t end_cursor;
|
||||
LineAddress end_line; // r09_max_scanline_addr
|
||||
LineAddress start_cursor; // r10_cursor_start
|
||||
LineAddress end_cursor; // r11_cursor_end
|
||||
} vertical;
|
||||
|
||||
InterlaceMode interlace_mode_ = InterlaceMode::Off;
|
||||
uint8_t end_row() const {
|
||||
return interlace_mode_ == InterlaceMode::InterlaceSyncAndVideo ? vertical.end_row & ~1 : vertical.end_row;
|
||||
}
|
||||
InterlaceMode interlace_mode_ = InterlaceMode::Off; // r08_interlace
|
||||
|
||||
uint16_t start_address;
|
||||
uint16_t cursor_address;
|
||||
uint16_t light_pen_address;
|
||||
uint8_t cursor_flags;
|
||||
RefreshAddress start_address; // r12_start_addr_h + r13_start_addr_l
|
||||
RefreshAddress cursor_address; // r14_cursor_h + r15_cursor_l
|
||||
RefreshAddress light_pen_address; // r16_light_pen_h + r17_light_pen_l
|
||||
Numeric::SizedInt<2> cursor_flags; // r10_cursor_mode
|
||||
} layout_;
|
||||
|
||||
uint8_t registers_[18]{};
|
||||
uint8_t dummy_register_ = 0;
|
||||
int selected_register_ = 0;
|
||||
|
||||
uint8_t character_counter_ = 0;
|
||||
uint32_t character_reset_history_ = 0;
|
||||
uint8_t row_counter_ = 0, next_row_counter_ = 0;
|
||||
CharacterAddress character_counter_; // h_counter
|
||||
Numeric::SizedInt<3> character_reset_history_; // sol
|
||||
RowAddress row_counter_; // row_counter
|
||||
RowAddress next_row_counter_; // row_counter_next
|
||||
LineAddress line_; // line_counter
|
||||
LineAddress next_line_; // line_counter_next
|
||||
RefreshAddress refresh_; // ma_i
|
||||
uint8_t adjustment_counter_ = 0;
|
||||
|
||||
bool character_is_visible_ = false;
|
||||
bool line_is_visible_ = false;
|
||||
bool character_is_visible_ = false; // h_display
|
||||
bool row_is_visible_ = false; // v_display
|
||||
bool is_first_scanline_ = false;
|
||||
bool is_cursor_line_ = false;
|
||||
bool cursor_mask_ = false;
|
||||
|
||||
int hsync_counter_ = 0;
|
||||
int vsync_counter_ = 0;
|
||||
bool is_in_adjustment_period_ = false;
|
||||
SyncCounter hsync_counter_; // h_sync_counter
|
||||
SyncCounter vsync_counter_; // v_sync_counter
|
||||
bool will_adjust_ = false; // in_adj
|
||||
bool is_in_adjustment_period_ = false; // adj_in_progress
|
||||
|
||||
uint16_t line_address_ = 0;
|
||||
RefreshAddress line_address_; // ma_row
|
||||
uint8_t status_ = 0;
|
||||
|
||||
int display_skew_mask_ = 1;
|
||||
unsigned int character_is_visible_shifter_ = 0;
|
||||
|
||||
bool eof_latched_ = false;
|
||||
bool eom_latched_ = false;
|
||||
uint16_t next_row_address_ = 0;
|
||||
bool odd_field_ = false;
|
||||
bool eof_latched_ = false; // eof_latched
|
||||
bool eom_latched_ = false; // eom_latched
|
||||
bool odd_field_ = false; // odd_field
|
||||
bool extra_line_ = false; // extra_scanline
|
||||
|
||||
bool hit_vsync_last_ = false; // vs_hit_last
|
||||
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;
|
||||
|
||||
void update_cursor_mask() {
|
||||
switch(cursor_type) {
|
||||
case CursorType::None:
|
||||
break;
|
||||
|
||||
// MDA-style blinking.
|
||||
// https://retrocomputing.stackexchange.com/questions/27803/what-are-the-blinking-rates-of-the-caret-and-of-blinking-text-on-pc-graphics-car
|
||||
// gives an 8/8 pattern for regular blinking though mode 11 is then just a guess.
|
||||
case CursorType::MDA:
|
||||
switch(layout_.cursor_flags.get()) {
|
||||
case 0b11: cursor_mask_ = (bus_state_.field_count & 8) < 3; break;
|
||||
case 0b00: cursor_mask_ = bus_state_.field_count.bit<3>(); break;
|
||||
case 0b01: cursor_mask_ = false; break;
|
||||
case 0b10: cursor_mask_ = true; break;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
|
||||
// Standard built-in 6845 blinking.
|
||||
case CursorType::Native:
|
||||
switch(layout_.cursor_flags.get()) {
|
||||
case 0b00: cursor_mask_ = true; break;
|
||||
case 0b01: cursor_mask_ = false; break;
|
||||
case 0b10: cursor_mask_ = bus_state_.field_count.bit<3>(); break;
|
||||
case 0b11: cursor_mask_= bus_state_.field_count.bit<4>(); break;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "6850.hpp"
|
||||
|
||||
#include <bit>
|
||||
#include <cassert>
|
||||
|
||||
using namespace Motorola::ACIA;
|
||||
@@ -141,11 +142,8 @@ int ACIA::expected_bits() {
|
||||
return 1 + data_bits_ + stop_bits_ + (parity_ != Parity::None);
|
||||
}
|
||||
|
||||
uint8_t ACIA::parity(uint8_t value) {
|
||||
value ^= value >> 4;
|
||||
value ^= value >> 2;
|
||||
value ^= value >> 1;
|
||||
return value ^ (parity_ == Parity::Even);
|
||||
uint8_t ACIA::parity(const uint8_t value) {
|
||||
return (std::popcount(value) & 1) ^ (parity_ == Parity::Even);
|
||||
}
|
||||
|
||||
bool ACIA::serial_line_did_produce_bit(Serial::Line<false> *, const int bit) {
|
||||
|
351
Components/SAA5050/SAA5050.cpp
Normal file
351
Components/SAA5050/SAA5050.cpp
Normal file
@@ -0,0 +1,351 @@
|
||||
//
|
||||
// SAA5050.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 24/09/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "SAA5050.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
namespace {
|
||||
// SAA5050 font, padded out to one byte per row. The least-significant five bits of each byte
|
||||
// are the meaningful pixels for that row, with the LSB being on the right.
|
||||
constexpr uint8_t font[][10] = {
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, // Character 32.
|
||||
{0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x0a, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x06, 0x09, 0x08, 0x1c, 0x08, 0x08, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x15, 0x14, 0x0e, 0x05, 0x15, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03, 0x00, 0x00, },
|
||||
{0x00, 0x08, 0x14, 0x14, 0x08, 0x15, 0x12, 0x0d, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02, 0x00, 0x00, },
|
||||
{0x00, 0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x15, 0x0e, 0x04, 0x0e, 0x15, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x04, 0x04, 0x1f, 0x04, 0x04, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x08, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x0a, 0x11, 0x11, 0x11, 0x0a, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x01, 0x06, 0x08, 0x10, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x01, 0x02, 0x06, 0x01, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x04, 0x08, 0x00, },
|
||||
{0x00, 0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x02, 0x04, 0x04, 0x00, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x0a, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1e, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x10, 0x10, 0x13, 0x11, 0x0f, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0d, 0x00, 0x00, },
|
||||
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x10, 0x0e, 0x01, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x11, 0x0a, 0x0a, 0x04, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0a, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x0a, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x04, 0x08, 0x1f, 0x08, 0x04, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x10, 0x10, 0x10, 0x10, 0x16, 0x01, 0x02, 0x04, 0x07, },
|
||||
{0x00, 0x00, 0x04, 0x02, 0x1f, 0x02, 0x04, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x04, 0x0e, 0x15, 0x04, 0x04, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x0a, 0x0a, 0x1f, 0x0a, 0x1f, 0x0a, 0x0a, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0e, 0x01, 0x0f, 0x11, 0x0f, 0x00, 0x00, },
|
||||
{0x00, 0x10, 0x10, 0x1e, 0x11, 0x11, 0x11, 0x1e, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0f, 0x10, 0x10, 0x10, 0x0f, 0x00, 0x00, },
|
||||
{0x00, 0x01, 0x01, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0e, 0x11, 0x1f, 0x10, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x02, 0x04, 0x04, 0x0e, 0x04, 0x04, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x0e, },
|
||||
{0x00, 0x10, 0x10, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x00, 0x0c, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, },
|
||||
{0x00, 0x08, 0x08, 0x09, 0x0a, 0x0c, 0x0a, 0x09, 0x00, 0x00, },
|
||||
{0x00, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x1a, 0x15, 0x15, 0x15, 0x15, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0e, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x1e, 0x11, 0x11, 0x11, 0x1e, 0x10, 0x10, },
|
||||
{0x00, 0x00, 0x00, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x01, },
|
||||
{0x00, 0x00, 0x00, 0x0b, 0x0c, 0x08, 0x08, 0x08, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0f, 0x10, 0x0e, 0x01, 0x1e, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x04, 0x0e, 0x04, 0x04, 0x04, 0x02, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0f, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x11, 0x11, 0x0a, 0x0a, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0a, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x0e, },
|
||||
{0x00, 0x00, 0x00, 0x1f, 0x02, 0x04, 0x08, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x10, 0x10, 0x10, 0x10, 0x11, 0x03, 0x05, 0x07, 0x01, },
|
||||
{0x00, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x00, 0x00, },
|
||||
{0x00, 0x18, 0x04, 0x18, 0x04, 0x19, 0x03, 0x05, 0x07, 0x01, },
|
||||
{0x00, 0x00, 0x04, 0x00, 0x1f, 0x00, 0x04, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x00, 0x00, },
|
||||
};
|
||||
|
||||
enum ControlCode: uint8_t {
|
||||
RedAlpha = 0x01,
|
||||
GreenAlpha = 0x02,
|
||||
YellowAlpha = 0x03,
|
||||
BlueAlpha = 0x04,
|
||||
MagentaAlpha = 0x05,
|
||||
CyanAlpha = 0x06,
|
||||
WhiteAlpha = 0x07,
|
||||
|
||||
Flash = 0x08,
|
||||
Steady = 0x09,
|
||||
|
||||
RedGraphics = 0x11,
|
||||
GreenGraphics = 0x12,
|
||||
YellowGraphics = 0x13,
|
||||
BlueGraphics = 0x14,
|
||||
MagentaGraphics = 0x15,
|
||||
CyanGraphics = 0x16,
|
||||
WhiteGraphics = 0x17,
|
||||
|
||||
Conceal = 0x18,
|
||||
|
||||
ContinuousGraphics = 0x19,
|
||||
SeparatedGraphics = 0x1a,
|
||||
|
||||
NormalHeight = 0xc,
|
||||
DoubleHeight = 0xd,
|
||||
|
||||
BlackBackground = 0x1c,
|
||||
NewBackground = 0x1d,
|
||||
|
||||
HoldGraphics = 0x1e,
|
||||
ReleaseGraphics = 0x1f,
|
||||
};}
|
||||
|
||||
using namespace Mullard;
|
||||
|
||||
void SAA5050Serialiser::begin_frame(const bool is_odd) {
|
||||
line_ = -2;
|
||||
row_ = 0;
|
||||
odd_frame_ = is_odd;
|
||||
|
||||
row_has_double_height_ = false;
|
||||
double_height_offset_ = 0;
|
||||
|
||||
++frame_counter_;
|
||||
}
|
||||
|
||||
void SAA5050Serialiser::begin_line() {
|
||||
line_ += 2;
|
||||
if(line_ == 20) {
|
||||
line_ = 0;
|
||||
++row_;
|
||||
|
||||
if(row_has_double_height_) {
|
||||
double_height_offset_ = (double_height_offset_ + 5) % 10;
|
||||
}
|
||||
row_has_double_height_ = false;
|
||||
}
|
||||
|
||||
output_.reset();
|
||||
has_output_ = false;
|
||||
|
||||
apply_control(ControlCode::WhiteAlpha);
|
||||
apply_control(ControlCode::Steady);
|
||||
apply_control(ControlCode::NormalHeight);
|
||||
apply_control(ControlCode::ContinuousGraphics);
|
||||
apply_control(ControlCode::BlackBackground);
|
||||
apply_control(ControlCode::ReleaseGraphics);
|
||||
}
|
||||
|
||||
bool SAA5050Serialiser::has_output() const {
|
||||
return has_output_;
|
||||
}
|
||||
|
||||
SAA5050Serialiser::Output SAA5050Serialiser::output() {
|
||||
has_output_ = false;
|
||||
return output_;
|
||||
}
|
||||
|
||||
void SAA5050Serialiser::apply_control(const uint8_t value) {
|
||||
const auto set_alpha = [&](const uint8_t colour) {
|
||||
alpha_mode_ = true;
|
||||
conceal_ = false;
|
||||
output_.alpha = colour;
|
||||
hold_graphics_ = false;
|
||||
};
|
||||
|
||||
const auto set_graphics = [&](const uint8_t colour) {
|
||||
alpha_mode_ = false;
|
||||
conceal_ = false;
|
||||
output_.alpha = colour;
|
||||
hold_graphics_ = false;
|
||||
};
|
||||
|
||||
switch(value) {
|
||||
default: break;
|
||||
|
||||
case RedAlpha: set_alpha(0b100); break;
|
||||
case GreenAlpha: set_alpha(0b010); break;
|
||||
case YellowAlpha: set_alpha(0b110); break;
|
||||
case BlueAlpha: set_alpha(0b001); break;
|
||||
case MagentaAlpha: set_alpha(0b101); break;
|
||||
case CyanAlpha: set_alpha(0b011); break;
|
||||
case WhiteAlpha: set_alpha(0b111); break;
|
||||
|
||||
case Flash: flash_ = true; break;
|
||||
case Steady: flash_ = false; break;
|
||||
|
||||
case RedGraphics: set_graphics(0b100); break;
|
||||
case GreenGraphics: set_graphics(0b010); break;
|
||||
case YellowGraphics: set_graphics(0b110); break;
|
||||
case BlueGraphics: set_graphics(0b001); break;
|
||||
case MagentaGraphics: set_graphics(0b101); break;
|
||||
case CyanGraphics: set_graphics(0b011); break;
|
||||
case WhiteGraphics: set_graphics(0b111); break;
|
||||
|
||||
case Conceal: conceal_ = true; break;
|
||||
|
||||
case ContinuousGraphics: separated_graphics_ = false; break;
|
||||
case SeparatedGraphics: separated_graphics_ = true; break;
|
||||
|
||||
case NormalHeight: double_height_ = false; break;
|
||||
case DoubleHeight: double_height_ = row_has_double_height_ = true; break;
|
||||
|
||||
case BlackBackground: output_.background = 0; break;
|
||||
case NewBackground: output_.background = output_.alpha; break;
|
||||
|
||||
case HoldGraphics: hold_graphics_ = true; break;
|
||||
case ReleaseGraphics: hold_graphics_ = false; last_graphic_ = 32; break;
|
||||
}
|
||||
}
|
||||
|
||||
void SAA5050Serialiser::set_reveal(const bool reveal) {
|
||||
reveal_ = reveal;
|
||||
}
|
||||
|
||||
void SAA5050Serialiser::add(const Numeric::SizedInt<7> c) {
|
||||
has_output_ = true;
|
||||
if(c.get() < 32) {
|
||||
if(hold_graphics_) {
|
||||
load_pixels(last_graphic_);
|
||||
} else {
|
||||
output_.reset();
|
||||
}
|
||||
apply_control(c.get());
|
||||
return;
|
||||
}
|
||||
load_pixels(c.get());
|
||||
}
|
||||
|
||||
void SAA5050Serialiser::load_pixels(const uint8_t c) {
|
||||
if(flash_ && ((frame_counter_&31) > 23)) { // Complete guess on the blink period here.
|
||||
output_.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if(conceal_ && !reveal_) {
|
||||
output_.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// Divert into graphics only if both the mode and the character code allows it.
|
||||
if(!alpha_mode_ && (c & (1 << 5))) {
|
||||
last_graphic_ = c;
|
||||
|
||||
// Graphics layout:
|
||||
//
|
||||
// |----|----|
|
||||
// | | |
|
||||
// | b0 | b1 |
|
||||
// | | |
|
||||
// |----|----|
|
||||
// | | |
|
||||
// | b2 | b3 |
|
||||
// | | |
|
||||
// |----|----|
|
||||
// | | |
|
||||
// | b4 | b6 |
|
||||
// | | |
|
||||
// |----|----|
|
||||
|
||||
if(separated_graphics_ && (line_ == 6 || line_ == 12 || line_ == 18)) {
|
||||
output_.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t pixels;
|
||||
if(line_ < 6) {
|
||||
pixels =
|
||||
((c & 1) ? 0b111'000 : 0) |
|
||||
((c & 2) ? 0b000'111 : 0);
|
||||
} else if(line_ < 14) {
|
||||
pixels =
|
||||
((c & 4) ? 0b111'000 : 0) |
|
||||
((c & 8) ? 0b000'111 : 0);
|
||||
} else {
|
||||
pixels =
|
||||
((c & 16) ? 0b111'000 : 0) |
|
||||
((c & 64) ? 0b000'111 : 0);
|
||||
}
|
||||
|
||||
if(separated_graphics_) {
|
||||
pixels &= 0b011'011;
|
||||
}
|
||||
|
||||
output_.load(pixels);
|
||||
return;
|
||||
}
|
||||
|
||||
if(double_height_) {
|
||||
const auto top_address = (line_ >> 2) + double_height_offset_;
|
||||
const uint8_t top = font[c - 32][top_address];
|
||||
const uint8_t bottom = font[c - 32][std::min(9, top_address + 1)];
|
||||
|
||||
if(line_ & 2) {
|
||||
output_.load(bottom, top);
|
||||
} else {
|
||||
output_.load(top, bottom);
|
||||
}
|
||||
} else {
|
||||
if(double_height_offset_) {
|
||||
output_.reset();
|
||||
} else {
|
||||
const auto top_address = line_ >> 1;
|
||||
const uint8_t top = font[c - 32][top_address];
|
||||
const uint8_t bottom = font[c - 32][std::min(9, top_address + 1)];
|
||||
|
||||
if(odd_frame_) {
|
||||
output_.load(bottom, top);
|
||||
} else {
|
||||
output_.load(top, bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
100
Components/SAA5050/SAA5050.hpp
Normal file
100
Components/SAA5050/SAA5050.hpp
Normal file
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// SAA5050.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 24/09/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "Numeric/SizedInt.hpp"
|
||||
|
||||
namespace Mullard {
|
||||
|
||||
struct SAA5050Serialiser {
|
||||
public:
|
||||
void begin_frame(bool is_odd);
|
||||
void begin_line();
|
||||
|
||||
void add(Numeric::SizedInt<7>);
|
||||
|
||||
struct Output {
|
||||
void reset() {
|
||||
top_ = bottom_ = 0;
|
||||
}
|
||||
void load(const uint8_t top, const uint8_t bottom) {
|
||||
top_ = top;
|
||||
bottom_ = bottom;
|
||||
}
|
||||
void load(const uint8_t top) {
|
||||
top_ = bottom_ = top;
|
||||
}
|
||||
|
||||
// The low twelve bits of this word provide 1bpp pixels.
|
||||
uint16_t pixels() const {
|
||||
// Adapted from old ElectrEm source; my original provenance for this being the correct logic is unknown.
|
||||
uint16_t wide =
|
||||
((top_ & 0b000001) ? 0b0000'0000'0011 : 0) |
|
||||
((top_ & 0b000010) ? 0b0000'0000'1100 : 0) |
|
||||
((top_ & 0b000100) ? 0b0000'0011'0000 : 0) |
|
||||
((top_ & 0b001000) ? 0b0000'1100'0000 : 0) |
|
||||
((top_ & 0b010000) ? 0b0011'0000'0000 : 0) |
|
||||
((top_ & 0b100000) ? 0b1100'0000'0000 : 0);
|
||||
|
||||
if(top_ != bottom_) {
|
||||
if((top_ & 0b10000) && (bottom_ & 0b11000) == 0b01000) wide |= 0b0000'1000'0000;
|
||||
if((top_ & 0b01000) && (bottom_ & 0b01100) == 0b00100) wide |= 0b0000'0010'0000;
|
||||
if((top_ & 0b00100) && (bottom_ & 0b00110) == 0b00010) wide |= 0b0000'0000'1000;
|
||||
if((top_ & 0b00010) && (bottom_ & 0b00011) == 0b00001) wide |= 0b0000'0000'0010;
|
||||
|
||||
if((top_ & 0b01000) && (bottom_ & 0b11000) == 0b10000) wide |= 0b0001'0000'0000;
|
||||
if((top_ & 0b00100) && (bottom_ & 0b01100) == 0b01000) wide |= 0b0000'0100'0000;
|
||||
if((top_ & 0b00010) && (bottom_ & 0b00110) == 0b00100) wide |= 0b0000'0001'0000;
|
||||
if((top_ & 0b00001) && (bottom_ & 0b00011) == 0b00010) wide |= 0b0000'0000'0100;
|
||||
}
|
||||
|
||||
return wide;
|
||||
}
|
||||
|
||||
// Colours for foreground and background pixels.
|
||||
uint8_t alpha;
|
||||
uint8_t background;
|
||||
|
||||
private:
|
||||
uint8_t top_, bottom_;
|
||||
};
|
||||
bool has_output() const;
|
||||
Output output();
|
||||
|
||||
void set_reveal(bool);
|
||||
|
||||
private:
|
||||
Output output_;
|
||||
bool has_output_ = false;
|
||||
|
||||
int row_, line_;
|
||||
bool odd_frame_;
|
||||
|
||||
bool flash_ = false;
|
||||
int frame_counter_ = 0;
|
||||
|
||||
bool reveal_ = false;
|
||||
bool conceal_ = false;
|
||||
|
||||
bool alpha_mode_ = true;
|
||||
bool separated_graphics_ = false;
|
||||
|
||||
bool double_height_ = false;
|
||||
bool row_has_double_height_ = false;
|
||||
int double_height_offset_ = 0;
|
||||
|
||||
bool hold_graphics_ = false;
|
||||
uint8_t last_graphic_ = 0;
|
||||
|
||||
void load_pixels(const uint8_t);
|
||||
void apply_control(const uint8_t);
|
||||
};
|
||||
|
||||
}
|
85
Components/uPD7002/uPD7002.cpp
Normal file
85
Components/uPD7002/uPD7002.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// uPD7002.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 17/09/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "uPD7002.hpp"
|
||||
|
||||
using namespace NEC;
|
||||
|
||||
uPD7002::uPD7002(const HalfCycles clock_rate) {
|
||||
// Per the BBC AUG: "8 bit conversions typically take 4 ms to complete whereas 10 bit
|
||||
// conversions typically take 10 ms to complete".
|
||||
fast_period_ = clock_rate / 250;
|
||||
slow_period_ = clock_rate / 100;
|
||||
}
|
||||
|
||||
void uPD7002::run_for(const HalfCycles count) {
|
||||
if(!conversion_time_remaining_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(count >= conversion_time_remaining_) {
|
||||
conversion_time_remaining_ = HalfCycles(0);
|
||||
result_ = uint16_t(inputs_[channel_] * 65535.0f) & (high_precision_ ? 0xfff0 : 0xff00);
|
||||
set_interrupt(true);
|
||||
return;
|
||||
}
|
||||
|
||||
conversion_time_remaining_ -= count;
|
||||
}
|
||||
|
||||
bool uPD7002::interrupt() const {
|
||||
return interrupt_;
|
||||
}
|
||||
|
||||
void uPD7002::write(const uint16_t address, const uint8_t value) {
|
||||
const auto local_address = address & 3;
|
||||
if(!local_address) {
|
||||
channel_ = value & 0b0000'0011;
|
||||
spare_ = value & 0b0000'0100;
|
||||
high_precision_ = value & 0b0000'1000;
|
||||
conversion_time_remaining_ = high_precision_ ? slow_period_ : fast_period_;
|
||||
set_interrupt(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t uPD7002::read(const uint16_t address) {
|
||||
switch(address & 3) {
|
||||
default:
|
||||
case 0: return status();
|
||||
case 1:
|
||||
set_interrupt(false);
|
||||
return uint8_t(result_ >> 8);
|
||||
case 2: return uint8_t(result_);
|
||||
case 3: return 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t uPD7002::status() const {
|
||||
return
|
||||
channel_ |
|
||||
spare_ |
|
||||
(high_precision_ ? 0x08 : 0) |
|
||||
((result_ >> 14) & 0x30) |
|
||||
(conversion_time_remaining_ > HalfCycles(0) ? 0x00 : 0x40) |
|
||||
(interrupt_ ? 0x00 : 0x80);
|
||||
}
|
||||
|
||||
void uPD7002::set_delegate(Delegate *const delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
|
||||
void uPD7002::set_interrupt(const bool value) {
|
||||
if(interrupt_ == value) return;
|
||||
interrupt_ = value;
|
||||
if(delegate_) delegate_->did_change_interrupt_status(*this);
|
||||
}
|
||||
|
||||
void uPD7002::set_input(const int channel, const float value) {
|
||||
inputs_[channel] = value;
|
||||
}
|
51
Components/uPD7002/uPD7002.hpp
Normal file
51
Components/uPD7002/uPD7002.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// uPD7002.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 17/09/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
namespace NEC {
|
||||
|
||||
class uPD7002 {
|
||||
public:
|
||||
/// Constructs a PD7002 that will receive @c run_for updates at the specified clock rate.
|
||||
uPD7002(HalfCycles clock_rate);
|
||||
void run_for(HalfCycles);
|
||||
|
||||
/// @returns The current state of the interrupt line.
|
||||
bool interrupt() const;
|
||||
|
||||
/// Defines a mean for an observer to receive notifications upon updates to the interrupt line.
|
||||
struct Delegate {
|
||||
virtual void did_change_interrupt_status(uPD7002 &) = 0;
|
||||
};
|
||||
void set_delegate(Delegate *);
|
||||
|
||||
void write(uint16_t address, uint8_t value);
|
||||
uint8_t read(uint16_t address);
|
||||
|
||||
/// Sets the floating point value, which should be in the range [0.0, 1.0], for the signal currently
|
||||
/// being supplied to @c channel.
|
||||
void set_input(int channel, float value);
|
||||
|
||||
private:
|
||||
float inputs_[4]{};
|
||||
uint16_t result_ = 0;
|
||||
bool interrupt_ = false;
|
||||
|
||||
uint8_t channel_ = 0, spare_ = 0;
|
||||
bool high_precision_ = false;
|
||||
|
||||
HalfCycles conversion_time_remaining_{};
|
||||
HalfCycles fast_period_, slow_period_;
|
||||
|
||||
uint8_t status() const;
|
||||
void set_interrupt(bool);
|
||||
Delegate *delegate_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
@@ -165,10 +165,11 @@ public:
|
||||
const bool is_analogue_axis = input.is_analogue_axis();
|
||||
if(is_digital_axis || is_analogue_axis) {
|
||||
const size_t required_size = size_t(input.info.control.index+1);
|
||||
if(stick_types_.size() < required_size) {
|
||||
stick_types_.resize(required_size);
|
||||
if(sticks_.size() < required_size) {
|
||||
sticks_.resize(required_size);
|
||||
}
|
||||
stick_types_[size_t(input.info.control.index)] = is_digital_axis ? StickType::Digital : StickType::Analogue;
|
||||
sticks_[size_t(input.info.control.index)].type =
|
||||
is_digital_axis ? Stick::Type::Digital : Stick::Type::Analogue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,26 +180,50 @@ public:
|
||||
|
||||
void set_input(const Input &input, const bool is_active) final {
|
||||
// If this is a digital setting to a digital property, just pass it along.
|
||||
if(input.is_button() || stick_types_[input.info.control.index] == StickType::Digital) {
|
||||
if(input.is_button() || sticks_[input.info.control.index].type == Stick::Type::Digital) {
|
||||
did_set_input(input, is_active);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise this is logically to an analogue axis; for now just use some
|
||||
// convenient hard-coded values. TODO: make these a function of time.
|
||||
using Type = Joystick::Input::Type;
|
||||
// Otherwise this is logically to an analogue axis; map appropriately.
|
||||
// TODO: make these a function of time.
|
||||
auto &stick = sticks_[input.info.control.index];
|
||||
stick.apply_digital(input, is_active);
|
||||
const auto analogue_value = [&](const int mask) {
|
||||
switch(mask) {
|
||||
default: return 0.5f;
|
||||
case 0b01: return digital_maximum();
|
||||
case 0b10: return digital_minimum();
|
||||
}
|
||||
};
|
||||
|
||||
switch(input.type) {
|
||||
default: did_set_input(input, is_active ? 1.0f : 0.0f); break;
|
||||
case Type::Left: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.1f : 0.5f); break;
|
||||
case Type::Right: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.9f : 0.5f); break;
|
||||
case Type::Up: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.1f : 0.5f); break;
|
||||
case Type::Down: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.9f : 0.5f); break;
|
||||
using enum Joystick::Input::Type;
|
||||
|
||||
default:
|
||||
did_set_input(input, is_active ? 1.0f : 0.0f);
|
||||
break;
|
||||
|
||||
case Left:
|
||||
case Right:
|
||||
did_set_input(
|
||||
Input(Horizontal, input.info.control.index),
|
||||
analogue_value(stick.digital_mask(Horizontal))
|
||||
);
|
||||
break;
|
||||
case Up:
|
||||
case Down:
|
||||
did_set_input(
|
||||
Input(Vertical, input.info.control.index),
|
||||
analogue_value(stick.digital_mask(Vertical))
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void set_input(const Input &input, const float value) final {
|
||||
// If this is an analogue setting to an analogue property, just pass it along.
|
||||
if(!input.is_button() && stick_types_[input.info.control.index] == StickType::Analogue) {
|
||||
if(!input.is_button() && sticks_[input.info.control.index].type == Stick::Type::Analogue) {
|
||||
did_set_input(input, value);
|
||||
return;
|
||||
}
|
||||
@@ -206,7 +231,9 @@ public:
|
||||
// Otherwise apply a threshold test to convert to digital, with remapping from axes to digital inputs.
|
||||
using Type = Joystick::Input::Type;
|
||||
switch(input.type) {
|
||||
default: did_set_input(input, value > 0.5f); break;
|
||||
default:
|
||||
did_set_input(input, value > 0.5f);
|
||||
break;
|
||||
case Type::Horizontal:
|
||||
did_set_input(Input(Type::Left, input.info.control.index), value <= 0.25f);
|
||||
did_set_input(Input(Type::Right, input.info.control.index), value >= 0.75f);
|
||||
@@ -221,15 +248,44 @@ public:
|
||||
protected:
|
||||
virtual void did_set_input([[maybe_unused]] const Input &input, [[maybe_unused]] float value) {}
|
||||
virtual void did_set_input([[maybe_unused]] const Input &input, [[maybe_unused]] bool value) {}
|
||||
virtual float digital_minimum() const { return 0.1f; }
|
||||
virtual float digital_maximum() const { return 0.9f; }
|
||||
|
||||
private:
|
||||
const std::vector<Input> inputs_;
|
||||
|
||||
enum class StickType {
|
||||
Digital,
|
||||
Analogue
|
||||
struct Stick {
|
||||
enum class Type {
|
||||
Digital,
|
||||
Analogue
|
||||
} type;
|
||||
|
||||
void apply_digital(const Input &input, const bool is_active) {
|
||||
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::Right: return 1 << 3;
|
||||
case Input::Type::Left: return 1 << 4;
|
||||
}
|
||||
} ();
|
||||
if(is_active) {
|
||||
digital_inputs_ |= mask;
|
||||
} else {
|
||||
digital_inputs_ &= ~mask;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
int digital_inputs_ = 0;
|
||||
};
|
||||
std::vector<StickType> stick_types_;
|
||||
std::vector<Stick> sticks_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -7,17 +7,24 @@
|
||||
//
|
||||
|
||||
#include "BBCMicro.hpp"
|
||||
#include "Keyboard.hpp"
|
||||
|
||||
#include "Activity/Source.hpp"
|
||||
|
||||
#include "Machines/MachineTypes.hpp"
|
||||
#include "Machines/Utility/MemoryFuzzer.hpp"
|
||||
#include "Machines/Utility/Typer.hpp"
|
||||
|
||||
#include "Processors/6502/6502.hpp"
|
||||
|
||||
#include "Components/6522/6522.hpp"
|
||||
#include "Components/6845/CRTC6845.hpp"
|
||||
#include "Components/SN76489/SN76489.hpp"
|
||||
#include "Components/6850/6850.hpp"
|
||||
#include "Components/SAA5050/SAA5050.hpp"
|
||||
#include "Components/SN76489/SN76489.hpp"
|
||||
#include "Components/uPD7002/uPD7002.hpp"
|
||||
|
||||
// TODO: factor this more appropriately.
|
||||
#include "Machines/Acorn/Electron/Plus3.hpp"
|
||||
|
||||
#include "Analyser/Static/Acorn/Target.hpp"
|
||||
#include "Outputs/Log.hpp"
|
||||
@@ -26,6 +33,8 @@
|
||||
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||||
#include "Concurrency/AsyncTaskQueue.hpp"
|
||||
|
||||
#include "Keyboard.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
@@ -37,15 +46,62 @@ namespace BBCMicro {
|
||||
namespace {
|
||||
using Logger = Log::Logger<Log::Source::BBCMicro>;
|
||||
|
||||
/*!
|
||||
Provides an analogue joystick with a single fire button.
|
||||
*/
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick(NEC::uPD7002 &adc, const int first_channel) :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Horizontal),
|
||||
Input(Input::Vertical),
|
||||
Input(Input::Fire)
|
||||
}),
|
||||
adc_(adc),
|
||||
first_channel_(first_channel) {}
|
||||
|
||||
void did_set_input(const Input &input, const float value) final {
|
||||
switch(input.type) {
|
||||
case Input::Horizontal:
|
||||
case Input::Vertical:
|
||||
adc_.set_input(first_channel_ + (input.type == Input::Vertical), 1.0f - value);
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void did_set_input(const Input &input, const bool is_active) final {
|
||||
if(input.type == Input::Fire) {
|
||||
fire_ = is_active;
|
||||
}
|
||||
}
|
||||
|
||||
bool fire() const {
|
||||
return fire_;
|
||||
}
|
||||
|
||||
private:
|
||||
float digital_minimum() const final {
|
||||
return 0.0f;
|
||||
}
|
||||
float digital_maximum() const final {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
NEC::uPD7002 &adc_;
|
||||
const int first_channel_;
|
||||
bool fire_ = false;
|
||||
};
|
||||
/*!
|
||||
Combines an SN76489 with an appropriate asynchronous queue and filtering speaker.
|
||||
*/
|
||||
struct Audio {
|
||||
Audio() :
|
||||
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_),
|
||||
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, 2),
|
||||
speaker_(sn76489_)
|
||||
{
|
||||
// I'm *VERY* unsure about this.
|
||||
// Combined with the additional divider specified above, implies this chip is clocked at 4Mhz.
|
||||
speaker_.set_input_rate(2'000'000.0f);
|
||||
}
|
||||
|
||||
@@ -54,16 +110,16 @@ struct Audio {
|
||||
}
|
||||
|
||||
TI::SN76489 *operator ->() {
|
||||
flush();
|
||||
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
|
||||
return &sn76489_;
|
||||
}
|
||||
|
||||
void operator +=(const HalfCycles duration) {
|
||||
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
|
||||
void operator +=(const Cycles duration) {
|
||||
time_since_update_ += duration;
|
||||
}
|
||||
|
||||
void flush() {
|
||||
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
@@ -75,7 +131,7 @@ private:
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
Outputs::Speaker::PullLowpass<TI::SN76489> speaker_;
|
||||
HalfCycles time_since_update_;
|
||||
Cycles time_since_update_;
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -109,13 +165,21 @@ struct SystemVIAPortHandler;
|
||||
using SystemVIA = MOS::MOS6522::MOS6522<SystemVIAPortHandler>;
|
||||
|
||||
struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
SystemVIAPortHandler(Audio &audio, VideoBaseAddress &video_base, SystemVIA &via) :
|
||||
audio_(audio), video_base_(video_base), via_(via)
|
||||
struct Delegate {
|
||||
virtual void strobe_lightpen() = 0;
|
||||
};
|
||||
|
||||
SystemVIAPortHandler(
|
||||
Audio &audio,
|
||||
VideoBaseAddress &video_base,
|
||||
SystemVIA &via,
|
||||
Delegate &delegate,
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks,
|
||||
const bool run_disk
|
||||
) :
|
||||
audio_(audio), video_base_(video_base), via_(via), joysticks_(joysticks), delegate_(delegate)
|
||||
{
|
||||
// Set initial mode to mode 0.
|
||||
set_key(7, true);
|
||||
set_key(8, true);
|
||||
set_key(9, true);
|
||||
set_key_flag(6, run_disk);
|
||||
}
|
||||
|
||||
// CA2: key pressed;
|
||||
@@ -157,7 +221,18 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
|
||||
// Update keyboard LEDs.
|
||||
if(mask >= 0x40) {
|
||||
Logger::info().append("CAPS: %d SHIFT: %d", bool(latch_ & 0x40), bool(latch_ & 0x40));
|
||||
const bool new_caps = latch_ & 0x80;
|
||||
const bool new_shift = latch_ & 0x40;
|
||||
|
||||
if(new_caps != caps_led_state_) {
|
||||
caps_led_state_ = new_caps;
|
||||
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_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,9 +241,12 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
if(port == MOS::MOS6522::Port::B) {
|
||||
// TODO:
|
||||
//
|
||||
// b4/5: joystick fire buttons;
|
||||
// b6/7: speech interrupt/ready inputs.
|
||||
return 0x3f; // b6 = b7 = 0 => no speech hardware.
|
||||
// b4/5: joystick fire buttons (0 = pressed);
|
||||
// b6/7: speech interrupt/ready inputs. (0 expected if no speech hardware)
|
||||
return
|
||||
0xf |
|
||||
(static_cast<Joystick *>(joysticks_[0].get())->fire() ? 0x00 : 0x10) |
|
||||
(static_cast<Joystick *>(joysticks_[1].get())->fire() ? 0x00 : 0x20);
|
||||
}
|
||||
|
||||
if(latch_ & LatchFlags::KeyboardIsScanning) {
|
||||
@@ -180,8 +258,23 @@ 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) {
|
||||
key_column(key)[key_row(key)] = pressed;
|
||||
set_key_flag(key, pressed);
|
||||
update_ca2();
|
||||
}
|
||||
|
||||
void clear_all_keys() {
|
||||
key_states_ = std::array<KeyRow, 16>{};
|
||||
update_ca2();
|
||||
}
|
||||
|
||||
@@ -199,6 +292,22 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
keyboard_scan_column_ = ending_column;
|
||||
}
|
||||
|
||||
void set_activity_observer(Activity::Observer *const observer) {
|
||||
activity_observer_ = observer;
|
||||
|
||||
if(activity_observer_) {
|
||||
activity_observer_->register_led(caps_led, Activity::Observer::LEDPresentation::Persistent);
|
||||
activity_observer_->register_led(shift_led, Activity::Observer::LEDPresentation::Persistent);
|
||||
|
||||
activity_observer_->set_led_status(caps_led, caps_led_state_);
|
||||
activity_observer_->set_led_status(shift_led, shift_led_state_);
|
||||
}
|
||||
}
|
||||
|
||||
bool caps_lock() const {
|
||||
return caps_led_state_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t latch_ = 0;
|
||||
enum LatchFlags: uint8_t {
|
||||
@@ -207,6 +316,7 @@ private:
|
||||
};
|
||||
|
||||
uint8_t port_a_output_ = 0;
|
||||
bool previous_cb2_ = false;
|
||||
|
||||
Audio &audio_;
|
||||
VideoBaseAddress &video_base_;
|
||||
@@ -219,6 +329,9 @@ private:
|
||||
std::array<KeyRow, 16> key_states_{};
|
||||
int keyboard_scan_column_ = 0;
|
||||
|
||||
void set_key_flag(const uint8_t key, const bool pressed) {
|
||||
key_column(key)[key_row(key)] = pressed;
|
||||
}
|
||||
KeyRow &key_column(const uint8_t key) {
|
||||
return key_states_[key & 0xf];
|
||||
}
|
||||
@@ -241,6 +354,15 @@ private:
|
||||
|
||||
via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two>(state);
|
||||
}
|
||||
|
||||
static inline const std::string caps_led = "CAPS";
|
||||
static inline const std::string shift_led = "SHIFT";
|
||||
bool caps_led_state_ = false;
|
||||
bool shift_led_state_ = false;
|
||||
Activity::Observer *activity_observer_ = nullptr;
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks_;
|
||||
Delegate &delegate_;
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -251,20 +373,29 @@ public:
|
||||
CRTCBusHandler(const uint8_t *const ram, SystemVIA &system_via) :
|
||||
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1),
|
||||
ram_(ram),
|
||||
system_via_(system_via) {}
|
||||
system_via_(system_via)
|
||||
{
|
||||
crt_.set_visible_area(crt_.get_rect_for_area(30, 256, 160, 800, 4.0f / 3.0f));
|
||||
}
|
||||
|
||||
void set_palette(const uint8_t value) {
|
||||
const auto index = value >> 4;
|
||||
Logger::info().append("Palette entry %d set to %x", index, value & 0xf);
|
||||
palette_[index] = uint8_t(
|
||||
7 ^ (
|
||||
((value & 0b100) >> 2) |
|
||||
((value & 0b001) << 2) |
|
||||
(value & 0b010)
|
||||
)
|
||||
);
|
||||
flash_flags_[size_t(index)] = value & 0b1000;
|
||||
}
|
||||
|
||||
void set_control(const uint8_t value) {
|
||||
Logger::info().append("Video control set to %x", value);
|
||||
cycle_length_ = (value & 0x10) ? 8 : 16;
|
||||
Logger::info().append("TODO: video control => flash %d", bool(value & 0x01));
|
||||
Logger::info().append("TODO: video control => teletext %d", bool(value & 0x02));
|
||||
Logger::info().append("TODO: video control => columns %d", (value >> 2) & 0x03);
|
||||
Logger::info().append("TODO: video control => cursor segment %d%d%d", bool(value & 0x80), bool(value & 0x40), bool(value & 0x20));
|
||||
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;
|
||||
cursor_mask_ = value & 0b1110'0000;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -272,60 +403,104 @@ public:
|
||||
bus state and determines what output to produce based on the current palette and mode.
|
||||
*/
|
||||
void perform_bus_cycle(const Motorola::CRTC::BusState &state) {
|
||||
static constexpr size_t PixelAllocationUnit = 480; // Is assumed to be a multiple of both 12 and 16.
|
||||
// i.e. a multiple of 48.
|
||||
static_assert(!(PixelAllocationUnit % 16));
|
||||
static_assert(!(PixelAllocationUnit % 12));
|
||||
|
||||
system_via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(state.vsync);
|
||||
|
||||
// bool print = false;
|
||||
// uint16_t start_address = 0x7c00;
|
||||
// int rows = 24;
|
||||
// if(print) {
|
||||
// for(int y = 0; y < rows; y++) {
|
||||
// for(int x = 0; x < 40; x++) {
|
||||
// printf("%c", ram_[start_address + y*40 + x]);
|
||||
// }
|
||||
// printf("\n");
|
||||
// }
|
||||
// }
|
||||
|
||||
// Count cycles since horizontal sync to insert a colour burst.
|
||||
if(state.hsync) {
|
||||
++cycles_into_hsync_;
|
||||
} else {
|
||||
cycles_into_hsync_ = 0;
|
||||
}
|
||||
const bool is_colour_burst = (cycles_into_hsync_ >= 5 && cycles_into_hsync_ < 9);
|
||||
// TODO: this is copy/pasted from the CPC. How does the BBC do it?
|
||||
// if(state.hsync) {
|
||||
// ++cycles_into_hsync_;
|
||||
// } else {
|
||||
// cycles_into_hsync_ = 0;
|
||||
// }
|
||||
// const bool is_colour_burst = cycles_into_hsync_ >= 5 && cycles_into_hsync_ < 9;
|
||||
|
||||
// Sync is taken to override pixels, and is combined as a simple OR.
|
||||
const bool is_sync = state.hsync || state.vsync;
|
||||
const bool is_blank = !is_sync && state.hsync;
|
||||
|
||||
OutputMode output_mode;
|
||||
if(is_sync) {
|
||||
output_mode = OutputMode::Sync;
|
||||
} else if(is_colour_burst) {
|
||||
output_mode = OutputMode::ColourBurst;
|
||||
} else if(is_blank) {
|
||||
output_mode = OutputMode::Blank;
|
||||
} else if(state.display_enable) {
|
||||
output_mode = OutputMode::Pixels;
|
||||
} else {
|
||||
output_mode = OutputMode::Border;
|
||||
// Check for a cursor leading edge.
|
||||
cursor_shifter_ >>= 4;
|
||||
if(state.cursor != previous_cursor_enabled_) {
|
||||
if(state.cursor) {
|
||||
cursor_shifter_ =
|
||||
((cursor_mask_ & 0x80) ? 0x0007 : 0) |
|
||||
((cursor_mask_ & 0x40) ? 0x0070 : 0) |
|
||||
((cursor_mask_ & 0x20) ? 0x7700 : 0);
|
||||
}
|
||||
previous_cursor_enabled_ = state.cursor;
|
||||
}
|
||||
|
||||
// Consider some SAA5050 signalling.
|
||||
if(!state.vsync && previous_vsync_) {
|
||||
// Complete fiction here; the SAA5050 field flag is set by peeking inside CRTC state.
|
||||
// TODO: what really sets CRS for the SAA5050? Time since hsync maybe?
|
||||
saa5050_serialiser_.begin_frame(state.field_count.bit<0>());
|
||||
}
|
||||
previous_vsync_ = state.vsync;
|
||||
|
||||
if(state.display_enable && !previous_display_enabled_ && active_collation_.is_teletext) {
|
||||
saa5050_serialiser_.begin_line();
|
||||
}
|
||||
previous_display_enabled_ = state.display_enable;
|
||||
|
||||
// Grab 5050 output, if any.
|
||||
bool has_5050_output_ = saa5050_serialiser_.has_output();
|
||||
const auto saa_50505_output_ = saa5050_serialiser_.output();
|
||||
|
||||
// Fetch, possibly.
|
||||
const bool should_fetch = state.display_enable && (active_collation_.is_teletext || !(state.line.get() & 8));
|
||||
if(should_fetch) {
|
||||
const uint16_t address = [&] {
|
||||
// Teletext address generation.
|
||||
if(state.refresh.get() & (1 << 13)) {
|
||||
return uint16_t(
|
||||
0x3c00 |
|
||||
((state.refresh.get() & 0x800) << 3) |
|
||||
(state.refresh.get() & 0x3ff)
|
||||
);
|
||||
}
|
||||
|
||||
uint16_t address = uint16_t((state.refresh.get() << 3) | (state.line.get() & 7));
|
||||
if(address & 0x8000) {
|
||||
address = (address + video_base_) & 0x7fff;
|
||||
}
|
||||
return address;
|
||||
} ();
|
||||
const uint8_t fetched = ram_[address];
|
||||
pixel_shifter_ = fetched;
|
||||
saa5050_serialiser_.add(fetched);
|
||||
}
|
||||
|
||||
// Pick new output mode.
|
||||
const OutputMode output_mode = [&] {
|
||||
if(state.hsync || state.vsync) {
|
||||
return OutputMode::Sync;
|
||||
}
|
||||
// if(is_colour_burst) {
|
||||
// return OutputMode::ColourBurst;
|
||||
// }
|
||||
if(
|
||||
(should_fetch && !active_collation_.is_teletext) ||
|
||||
(has_5050_output_ && active_collation_.is_teletext) ||
|
||||
cursor_shifter_
|
||||
) {
|
||||
return OutputMode::Pixels;
|
||||
}
|
||||
return OutputMode::Blank;
|
||||
} ();
|
||||
|
||||
// If a transition between sync/border/pixels just occurred, flush whatever was
|
||||
// in progress to the CRT and reset counting.
|
||||
if(output_mode != previous_output_mode_) {
|
||||
// in progress to the CRT and reset counting. Also flush if this mode has just been effective
|
||||
// for a really long time, so as not to buffer too much.
|
||||
if(output_mode != previous_output_mode_ || cycles_ == 1024) {
|
||||
if(cycles_) {
|
||||
switch(previous_output_mode_) {
|
||||
default:
|
||||
case OutputMode::Blank: crt_.output_blank(cycles_); break;
|
||||
case OutputMode::Sync: crt_.output_sync(cycles_); break;
|
||||
case OutputMode::Border: crt_.output_blank(cycles_); break;
|
||||
case OutputMode::ColourBurst: crt_.output_default_colour_burst(cycles_); break;
|
||||
case OutputMode::Pixels:
|
||||
crt_.output_data(cycles_, pixels_);
|
||||
pixel_pointer_ = pixel_data_ = nullptr;
|
||||
pixels_ = 0;
|
||||
break;
|
||||
case OutputMode::Pixels: flush_pixels(); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,61 +508,50 @@ public:
|
||||
previous_output_mode_ = output_mode;
|
||||
}
|
||||
|
||||
// Increment cycles since state changed.
|
||||
cycles_ += cycle_length_;
|
||||
|
||||
// Collect some more pixels if output is ongoing.
|
||||
if(previous_output_mode_ == OutputMode::Pixels) {
|
||||
if(!pixel_data_) {
|
||||
pixel_pointer_ = pixel_data_ = crt_.begin_data(320, 8);
|
||||
if(output_mode == OutputMode::Pixels) {
|
||||
// Flush the current buffer pixel if full; the CRTC allows many different display
|
||||
// widths so it's not necessarily possible to predict the correct number in advance
|
||||
// and using the upper bound could lead to inefficient behaviour.
|
||||
if(pixel_data_ && (pixels_collected() == PixelAllocationUnit || active_collation_ != previous_collation_)) {
|
||||
flush_pixels();
|
||||
cycles_ = 0;
|
||||
}
|
||||
if(pixel_pointer_) {
|
||||
uint16_t address;
|
||||
previous_collation_ = active_collation_;
|
||||
|
||||
if(state.refresh_address & (1 << 13)) {
|
||||
// Teletext address generation mode.
|
||||
address = uint16_t(
|
||||
0x3c00 |
|
||||
((state.refresh_address & 0x800) << 3) |
|
||||
(state.refresh_address & 0x3ff)
|
||||
);
|
||||
// TODO: wraparound?
|
||||
} else {
|
||||
address = uint16_t((state.refresh_address << 3) | (state.row_address & 7));
|
||||
if(address & 0x8000) {
|
||||
address = (address + video_base_) & 0x7fff;
|
||||
if(!pixel_data_) {
|
||||
pixel_pointer_ = pixel_data_ = crt_.begin_data(PixelAllocationUnit);
|
||||
}
|
||||
|
||||
if(pixel_data_) {
|
||||
if(active_collation_.is_teletext) {
|
||||
if(has_5050_output_) {
|
||||
uint16_t pixels = saa_50505_output_.pixels();
|
||||
for(int c = 0; c < 12; c++) {
|
||||
*pixel_pointer_++ =
|
||||
((pixels & 0b1000'0000'0000) ? saa_50505_output_.alpha : saa_50505_output_.background)
|
||||
^ uint8_t(cursor_shifter_);
|
||||
pixels <<= 1;
|
||||
}
|
||||
} else {
|
||||
std::fill(pixel_pointer_, pixel_pointer_ + 12, 0);
|
||||
pixel_pointer_ += 12;
|
||||
}
|
||||
}
|
||||
|
||||
// Hard coded from here for Mode 0!
|
||||
if(state.row_address & 8) {
|
||||
std::fill(pixel_pointer_, pixel_pointer_+8, 0);
|
||||
} else {
|
||||
const auto source = ram_[address];
|
||||
pixel_pointer_[0] = (source & 0x80) ? 0xff : 0x00;
|
||||
pixel_pointer_[1] = (source & 0x40) ? 0xff : 0x00;
|
||||
pixel_pointer_[2] = (source & 0x20) ? 0xff : 0x00;
|
||||
pixel_pointer_[3] = (source & 0x10) ? 0xff : 0x00;
|
||||
pixel_pointer_[4] = (source & 0x08) ? 0xff : 0x00;
|
||||
pixel_pointer_[5] = (source & 0x04) ? 0xff : 0x00;
|
||||
pixel_pointer_[6] = (source & 0x02) ? 0xff : 0x00;
|
||||
pixel_pointer_[7] = (source & 0x01) ? 0xff : 0x00;
|
||||
}
|
||||
|
||||
pixel_pointer_ += 8;
|
||||
pixels_ += 8;
|
||||
|
||||
// Flush the current buffer pixel if full; the CRTC allows many different display
|
||||
// widths so it's not necessarily possible to predict the correct number in advance
|
||||
// and using the upper bound could lead to inefficient behaviour.
|
||||
if(pixels_ == 320) {
|
||||
crt_.output_data(cycles_, pixels_);
|
||||
pixel_pointer_ = pixel_data_ = nullptr;
|
||||
cycles_ = 0;
|
||||
pixels_ = 0;
|
||||
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;
|
||||
case 8: shift_pixels<8>(cursor_shifter_ & 7); break;
|
||||
case 16: shift_pixels<16>(cursor_shifter_ & 7); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Increment cycles since state changed.
|
||||
cycles_ += active_collation_.crtc_clock_multiplier << 3;
|
||||
}
|
||||
|
||||
/// Sets the destination for output.
|
||||
@@ -410,40 +574,99 @@ public:
|
||||
return crt_.get_display_type();
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
enum class OutputMode {
|
||||
Sync,
|
||||
Blank,
|
||||
ColourBurst,
|
||||
Border,
|
||||
Pixels
|
||||
} previous_output_mode_ = OutputMode::Sync;
|
||||
};
|
||||
struct PixelCollation {
|
||||
int crtc_clock_multiplier = 1;
|
||||
int pixels_per_clock = 4;
|
||||
bool is_teletext = false;
|
||||
|
||||
bool operator !=(const PixelCollation &rhs) {
|
||||
// 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 cycle_length_ = 8;
|
||||
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr;
|
||||
size_t pixels_;
|
||||
size_t pixels_collected() const {
|
||||
return size_t(pixel_pointer_ - pixel_data_);
|
||||
}
|
||||
void flush_pixels() {
|
||||
crt_.output_data(cycles_, pixels_collected());
|
||||
pixel_pointer_ = pixel_data_ = nullptr;
|
||||
}
|
||||
PixelCollation previous_collation_;
|
||||
uint8_t palette_[16];
|
||||
std::bitset<16> flash_flags_;
|
||||
uint8_t flash_mask_ = 0;
|
||||
|
||||
PixelCollation active_collation_;
|
||||
uint8_t pixel_shifter_ = 0;
|
||||
|
||||
uint8_t cursor_mask_ = 0;
|
||||
uint32_t cursor_shifter_ = 0;
|
||||
bool previous_cursor_enabled_ = false;
|
||||
|
||||
bool previous_display_enabled_ = false;
|
||||
bool previous_vsync_ = false;
|
||||
|
||||
template <int count> void shift_pixels(const uint8_t cursor_mask) {
|
||||
for(int c = 0; c < count; c++) {
|
||||
const uint8_t colour =
|
||||
((pixel_shifter_ & 0x80) >> 4) |
|
||||
((pixel_shifter_ & 0x20) >> 3) |
|
||||
((pixel_shifter_ & 0x08) >> 2) |
|
||||
((pixel_shifter_ & 0x02) >> 1);
|
||||
pixel_shifter_ <<= 1;
|
||||
*pixel_pointer_++ = palette_[colour] ^ (flash_flags_[colour] ? flash_mask_ : 0x00) ^ cursor_mask;
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t *const ram_ = nullptr;
|
||||
SystemVIA &system_via_;
|
||||
|
||||
Mullard::SAA5050Serialiser saa5050_serialiser_;
|
||||
};
|
||||
using CRTC = Motorola::CRTC::CRTC6845<
|
||||
CRTCBusHandler,
|
||||
Motorola::CRTC::Personality::HD6845S,
|
||||
Motorola::CRTC::CursorType::None>;
|
||||
Motorola::CRTC::CursorType::Native>;
|
||||
}
|
||||
|
||||
template <bool has_1770>
|
||||
class ConcreteMachine:
|
||||
public Activity::Source,
|
||||
public Machine,
|
||||
public MachineTypes::AudioProducer,
|
||||
public MachineTypes::JoystickMachine,
|
||||
public MachineTypes::MappedKeyboardMachine,
|
||||
public MachineTypes::MediaTarget,
|
||||
public MachineTypes::ScanProducer,
|
||||
public MachineTypes::TimedMachine,
|
||||
public MOS::MOS6522::IRQDelegatePortHandler::Delegate
|
||||
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
|
||||
public NEC::uPD7002::Delegate,
|
||||
public SystemVIAPortHandler::Delegate,
|
||||
public Utility::TypeRecipient<CharacterMapper>,
|
||||
public WD::WD1770::Delegate
|
||||
{
|
||||
public:
|
||||
ConcreteMachine(
|
||||
@@ -451,22 +674,36 @@ public:
|
||||
const ROMMachine::ROMFetcher &rom_fetcher
|
||||
) :
|
||||
m6502_(*this),
|
||||
system_via_port_handler_(audio_, crtc_bus_handler_, system_via_),
|
||||
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_(crtc_bus_handler_),
|
||||
acia_(HalfCycles(2'000'000)) // TODO: look up real ACIA clock rate.
|
||||
acia_(HalfCycles(2'000'000)), // TODO: look up real ACIA clock rate.
|
||||
adc_(HalfCycles(2'000'000))
|
||||
{
|
||||
set_clock_rate(2'000'000);
|
||||
|
||||
// Install two joysticks.
|
||||
joysticks_.emplace_back(new Joystick(adc_, 0));
|
||||
joysticks_.emplace_back(new Joystick(adc_, 2));
|
||||
|
||||
system_via_port_handler_.set_interrupt_delegate(this);
|
||||
user_via_port_handler_.set_interrupt_delegate(this);
|
||||
adc_.set_delegate(this);
|
||||
|
||||
// Grab ROMs.
|
||||
using Request = ::ROM::Request;
|
||||
using Name = ::ROM::Name;
|
||||
const auto request = Request(Name::AcornBASICII) && Request(Name::BBCMicroMOS12);
|
||||
|
||||
auto request = Request(Name::AcornBASICII) && Request(Name::BBCMicroMOS12);
|
||||
if(target.has_1770dfs) {
|
||||
request = request && Request(Name::BBCMicroDFS226);
|
||||
}
|
||||
if(target.has_adfs) {
|
||||
request = request && Request(Name::BBCMicroADFS130);
|
||||
}
|
||||
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
@@ -475,8 +712,27 @@ public:
|
||||
const auto os_data = roms.find(Name::BBCMicroMOS12)->second;
|
||||
std::copy(os_data.begin(), os_data.end(), os_.begin());
|
||||
|
||||
// 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.
|
||||
size_t fs_slot = 14;
|
||||
if(target.has_1770dfs) {
|
||||
install_sideways(fs_slot--, roms.find(Name::BBCMicroDFS226)->second, false);
|
||||
}
|
||||
if(target.has_adfs) {
|
||||
install_sideways(fs_slot--, roms.find(Name::BBCMicroADFS130)->second, false);
|
||||
}
|
||||
|
||||
// Throw sideways RAM into all unused slots.
|
||||
if(target.has_sideways_ram) {
|
||||
for(size_t c = 0; c < 16; c++) {
|
||||
if(!rom_inserted_[c]) {
|
||||
rom_inserted_[c] = rom_write_masks_[c] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup fixed parts of memory map.
|
||||
page(0, &ram_[0], true);
|
||||
page(1, &ram_[16384], true);
|
||||
@@ -484,7 +740,14 @@ public:
|
||||
page(3, os_.data(), false);
|
||||
Memory::Fuzz(ram_);
|
||||
|
||||
(void)target;
|
||||
if constexpr (has_1770) {
|
||||
wd1770_.set_delegate(this);
|
||||
}
|
||||
|
||||
insert_media(target.media);
|
||||
if(!target.loading_command.empty()) {
|
||||
type_string(target.loading_command);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 6502 bus.
|
||||
@@ -516,7 +779,8 @@ public:
|
||||
};
|
||||
|
||||
// Determine whether this access hits the 1Mhz bus; if so then apply appropriate penalty, and update phase.
|
||||
const auto duration = is_1mhz(address) ? Cycles(2 + (phase_&1)) : Cycles(1);
|
||||
const auto duration = Cycles(is_1mhz(address) ? 2 + (phase_&1) : 1);
|
||||
if(typer_) typer_->run_for(duration);
|
||||
phase_ += duration.as<int>();
|
||||
|
||||
|
||||
@@ -524,7 +788,6 @@ public:
|
||||
// 1Mhz devices.
|
||||
//
|
||||
const auto half_cycles = HalfCycles(duration.as_integral());
|
||||
audio_ += half_cycles;
|
||||
system_via_.run_for(half_cycles);
|
||||
system_via_port_handler_.advance_keyboard_scan(half_cycles);
|
||||
user_via_.run_for(half_cycles);
|
||||
@@ -533,7 +796,7 @@ public:
|
||||
//
|
||||
// 2Mhz devices.
|
||||
//
|
||||
// TODO: if CRTC clock is 1Mhz, adapt.
|
||||
audio_ += duration;
|
||||
if(crtc_2mhz_) {
|
||||
crtc_.run_for(duration);
|
||||
} else {
|
||||
@@ -541,8 +804,14 @@ public:
|
||||
const auto cycles = (phase_ >> 1) - ((phase_ - duration.as<int>()) >> 1);
|
||||
crtc_.run_for(Cycles(cycles));
|
||||
}
|
||||
adc_.run_for(duration);
|
||||
|
||||
|
||||
if constexpr (has_1770) {
|
||||
// The WD1770 is nominally clocked at 8Mhz.
|
||||
wd1770_.run_for(duration * 4);
|
||||
}
|
||||
|
||||
//
|
||||
// Questionably-clocked devices.
|
||||
//
|
||||
@@ -552,7 +821,6 @@ public:
|
||||
//
|
||||
// Check for an IO access; if found then perform that and exit.
|
||||
//
|
||||
// static bool log = false;
|
||||
if(address >= 0xfc00 && address < 0xff00) {
|
||||
if(address >= 0xfe40 && address < 0xfe60) {
|
||||
if(is_read(operation)) {
|
||||
@@ -615,6 +883,27 @@ public:
|
||||
// Logger::info().append("ACIA write: %02x", *value);
|
||||
acia_.write(address, *value);
|
||||
}
|
||||
} else if(address >= 0xfec0 && address < 0xfee0) {
|
||||
if(is_read(operation)) {
|
||||
*value = adc_.read(address);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if(is_read(operation)) {
|
||||
*value = wd1770_.read(address);
|
||||
} else {
|
||||
wd1770_.write(address, *value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Logger::error()
|
||||
@@ -627,14 +916,6 @@ public:
|
||||
//
|
||||
// ROM or RAM access.
|
||||
//
|
||||
// if(operation == CPU::MOS6502Esque::BusOperation::ReadOpcode) {
|
||||
// log |= address == 0xc4c0;
|
||||
//
|
||||
// if(log) {
|
||||
// printf("%04x\n", address);
|
||||
// }
|
||||
// }
|
||||
|
||||
if(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_) {
|
||||
@@ -645,10 +926,6 @@ public:
|
||||
} else {
|
||||
if(memory_write_masks_[address >> 14]) {
|
||||
memory_[address >> 14][address] = *value;
|
||||
|
||||
if(address >= 0x7c00 && *value) {
|
||||
Logger::info().append("Output character: %c", *value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,6 +933,14 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
// MARK: - Activity::Source.
|
||||
void set_activity_observer(Activity::Observer *const observer) override {
|
||||
if(has_1770) {
|
||||
wd1770_.set_activity_observer(observer);
|
||||
}
|
||||
system_via_port_handler_.set_activity_observer(observer);
|
||||
}
|
||||
|
||||
// MARK: - AudioProducer.
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
return audio_.speaker();
|
||||
@@ -670,6 +955,12 @@ private:
|
||||
return crtc_bus_handler_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
|
||||
// MARK: - SystemVIAPortHandler::Delegate.
|
||||
void strobe_lightpen() override {
|
||||
crtc_.trigger_light_pen();
|
||||
}
|
||||
|
||||
// MARK: - KeyboardMachine.
|
||||
BBCMicro::KeyboardMapper mapper_;
|
||||
KeyboardMapper *get_keyboard_mapper() override {
|
||||
@@ -677,12 +968,63 @@ private:
|
||||
}
|
||||
|
||||
void set_key_state(const uint16_t key, const bool is_pressed) override {
|
||||
if(key == BBCMicro::KeyboardMapper::KeyBreak) {
|
||||
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:
|
||||
m6502_.set_reset_line(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);
|
||||
system_via_port_handler_.clear_all_keys();
|
||||
}
|
||||
|
||||
HalfCycles get_typer_delay(const std::string &text) const final {
|
||||
if(!m6502_.get_is_resetting()) {
|
||||
return Cycles(0);
|
||||
}
|
||||
|
||||
// Add a longer delay for a command at reset that involves pressing a modifier;
|
||||
// empirically this seems to be a requirement, in order to avoid a collision with
|
||||
// the system's built-in modifier-at-startup test (e.g. to perform shift+break).
|
||||
CharacterMapper test_mapper;
|
||||
const uint16_t *const sequence = test_mapper.sequence_for_character(text[0]);
|
||||
return is_modifier(Key(sequence[0])) ? Cycles(1'000'000) : Cycles(750'000);
|
||||
}
|
||||
|
||||
HalfCycles get_typer_frequency() const final {
|
||||
return Cycles(60'000);
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient<CharacterMapper>::add_typer(string);
|
||||
}
|
||||
|
||||
bool can_type(const char c) const final {
|
||||
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
|
||||
}
|
||||
|
||||
// MARK: - TimedMachine.
|
||||
void run_for(const Cycles cycles) override {
|
||||
@@ -700,6 +1042,20 @@ private:
|
||||
update_irq_line();
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
// MARK: - MediaTarget.
|
||||
bool insert_media(const Analyser::Static::Media &media) override {
|
||||
if(!media.disks.empty() && has_1770) {
|
||||
wd1770_.set_disk(media.disks.front(), 0);
|
||||
}
|
||||
|
||||
return !media.disks.empty();
|
||||
}
|
||||
|
||||
// MARK: - Clock phase.
|
||||
int phase_ = 0;
|
||||
|
||||
@@ -755,6 +1111,20 @@ private:
|
||||
bool crtc_2mhz_ = true;
|
||||
|
||||
Motorola::ACIA::ACIA acia_;
|
||||
|
||||
NEC::uPD7002 adc_;
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
// MARK: - Joysticks
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
return joysticks_;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -767,5 +1137,9 @@ std::unique_ptr<Machine> Machine::BBCMicro(
|
||||
) {
|
||||
using Target = Analyser::Static::Acorn::BBCMicroTarget;
|
||||
const Target *const acorn_target = dynamic_cast<const Target *>(target);
|
||||
return std::make_unique<BBCMicro::ConcreteMachine>(*acorn_target, rom_fetcher);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@@ -9,62 +9,244 @@
|
||||
#pragma once
|
||||
|
||||
#include "Machines/KeyboardMachine.hpp"
|
||||
#include "Machines/Utility/Typer.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace BBCMicro {
|
||||
|
||||
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
static constexpr uint16_t KeyBreak = 0xfffd;
|
||||
enum class Key: uint16_t {
|
||||
Escape = 0x70, Q = 0x10, F0 = 0x20, k1 = 0x30,
|
||||
CapsLock = 0x40, ShiftLock = 0x50, Tab = 0x60, Shift = 0x00,
|
||||
F1 = 0x71, k3 = 0x11, W = 0x21, k2 = 0x31,
|
||||
A = 0x41, S = 0x51, Z = 0x61, Control = 0x01,
|
||||
F2 = 0x72, k4 = 0x12, E = 0x22, D = 0x32,
|
||||
X = 0x42, C = 0x52, Space = 0x62, Bit7 = 0x02,
|
||||
F3 = 0x73, k5 = 0x13, T = 0x23, R = 0x33,
|
||||
F = 0x43, G = 0x53, V = 0x63, Bit6 = 0x03,
|
||||
F5 = 0x74, F4 = 0x14, k7 = 0x24, k6 = 0x34,
|
||||
Y = 0x44, H = 0x54, B = 0x64, Bit5 = 0x04,
|
||||
F6 = 0x75, k8 = 0x15, I = 0x25, U = 0x35,
|
||||
J = 0x45, N = 0x55, M = 0x65, Bit4 = 0x05,
|
||||
F8 = 0x76, F7 = 0x16, k9 = 0x26, O = 0x36,
|
||||
K = 0x46, L = 0x56, Comma = 0x66, Bit3 = 0x06,
|
||||
F9 = 0x77, Hyphen = 0x17, k0 = 0x27, P = 0x37,
|
||||
At = 0x47, Semicolon = 0x57, FullStop = 0x67, Bit2 = 0x07,
|
||||
|
||||
Backslash = 0x78,
|
||||
Caret = 0x18,
|
||||
Underscore = 0x28,
|
||||
OpenSquareBracket = 0x38,
|
||||
Colon = 0x48,
|
||||
CloseSquareBracket = 0x58,
|
||||
ForwardSlash = 0x68,
|
||||
Bit1 = 0x08,
|
||||
|
||||
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 = 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,
|
||||
|
||||
Alt = 0x02,
|
||||
LeftShift = 0x03,
|
||||
LeftControl = 0x04,
|
||||
LeftAlt = 0x05,
|
||||
RightShift = 0x06,
|
||||
RightControl = 0x07,
|
||||
RightAlt = 0x08,
|
||||
MouseSelect = 0x09,
|
||||
MouseMenu = 0x0a,
|
||||
MouseAdjust = 0x0b,
|
||||
};
|
||||
|
||||
constexpr bool is_modifier(const Key key) {
|
||||
return key == Key::Shift || key == Key::Control;
|
||||
}
|
||||
|
||||
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(const Inputs::Keyboard::Key key) const override {
|
||||
const auto found = key_map.find(key);
|
||||
return found != key_map.end() ? found->second : MachineTypes::MappedKeyboardMachine::KeyNotMapped;
|
||||
return found != key_map.end() ? uint16_t(found->second) : MachineTypes::MappedKeyboardMachine::KeyNotMapped;
|
||||
}
|
||||
|
||||
private:
|
||||
using Key = Inputs::Keyboard::Key;
|
||||
static inline const std::unordered_map<Key, uint16_t> key_map{
|
||||
{Key::Escape, 0x70},
|
||||
{Key::F12, KeyBreak},
|
||||
using CLKKey = Inputs::Keyboard::Key;
|
||||
static inline const std::unordered_map<CLKKey, Key> key_map{
|
||||
{CLKKey::Escape, Key::Escape},
|
||||
{CLKKey::F12, Key::Break},
|
||||
|
||||
{Key::F10, 0x20}, {Key::F1, 0x71}, {Key::F2, 0x72}, {Key::F3, 0x73}, {Key::F4, 0x14},
|
||||
{Key::F5, 0x75}, {Key::F6, 0x75}, {Key::F7, 0x16}, {Key::F8, 0x76}, {Key::F9, 0x77},
|
||||
// These are all wilfully off-by-one to approximate correct layout.
|
||||
{CLKKey::F1, Key::F0}, {CLKKey::F2, Key::F1}, {CLKKey::F3, Key::F2}, {CLKKey::F4, Key::F3},
|
||||
{CLKKey::F5, Key::F4}, {CLKKey::F6, Key::F5}, {CLKKey::F7, Key::F6}, {CLKKey::F8, Key::F7},
|
||||
{CLKKey::F9, Key::F8}, {CLKKey::F10, Key::F9},
|
||||
|
||||
{Key::Backslash, 0x78},
|
||||
{CLKKey::Backslash, Key::Backslash},
|
||||
|
||||
{Key::Left, 0x19}, {Key::Right, 0x79}, {Key::Up, 0x39}, {Key::Down, 0x29},
|
||||
{CLKKey::Left, Key::Left}, {CLKKey::Right, Key::Right}, {CLKKey::Up, Key::Up}, {CLKKey::Down, Key::Down},
|
||||
|
||||
{Key::Q, 0x10}, {Key::W, 0x21}, {Key::E, 0x22}, {Key::R, 0x33}, {Key::T, 0x23},
|
||||
{Key::Y, 0x44}, {Key::U, 0x35}, {Key::I, 0x25}, {Key::O, 0x36}, {Key::P, 0x37},
|
||||
{Key::A, 0x41}, {Key::S, 0x51}, {Key::D, 0x32}, {Key::F, 0x43}, {Key::G, 0x53},
|
||||
{Key::H, 0x54}, {Key::J, 0x45}, {Key::K, 0x46}, {Key::L, 0x56}, {Key::Z, 0x61},
|
||||
{Key::X, 0x42}, {Key::C, 0x52}, {Key::V, 0x63}, {Key::B, 0x64}, {Key::N, 0x55},
|
||||
{Key::M, 0x65},
|
||||
{CLKKey::Q, Key::Q}, {CLKKey::W, Key::W}, {CLKKey::E, Key::E}, {CLKKey::R, Key::R},
|
||||
{CLKKey::T, Key::T}, {CLKKey::Y, Key::Y}, {CLKKey::U, Key::U}, {CLKKey::I, Key::I},
|
||||
{CLKKey::O, Key::O}, {CLKKey::P, Key::P}, {CLKKey::A, Key::A}, {CLKKey::S, Key::S},
|
||||
{CLKKey::D, Key::D}, {CLKKey::F, Key::F}, {CLKKey::G, Key::G}, {CLKKey::H, Key::H},
|
||||
{CLKKey::J, Key::J}, {CLKKey::K, Key::K}, {CLKKey::L, Key::L}, {CLKKey::Z, Key::Z},
|
||||
{CLKKey::X, Key::X}, {CLKKey::C, Key::C}, {CLKKey::V, Key::V}, {CLKKey::B, Key::B},
|
||||
{CLKKey::N, Key::N}, {CLKKey::M, Key::M},
|
||||
|
||||
{Key::k0, 0x27}, {Key::k1, 0x30}, {Key::k2, 0x31}, {Key::k3, 0x11}, {Key::k4, 0x12},
|
||||
{Key::k5, 0x13}, {Key::k6, 0x34}, {Key::k7, 0x24}, {Key::k8, 0x15}, {Key::k9, 0x26},
|
||||
{CLKKey::k0, Key::k0}, {CLKKey::k1, Key::k1}, {CLKKey::k2, Key::k2}, {CLKKey::k3, Key::k3},
|
||||
{CLKKey::k4, Key::k4}, {CLKKey::k5, Key::k5}, {CLKKey::k6, Key::k6}, {CLKKey::k7, Key::k7},
|
||||
{CLKKey::k8, Key::k8}, {CLKKey::k9, Key::k9},
|
||||
|
||||
{Key::Comma, 0x66},
|
||||
{Key::FullStop, 0x67},
|
||||
{Key::ForwardSlash, 0x68},
|
||||
{CLKKey::Comma, Key::Comma},
|
||||
{CLKKey::FullStop, Key::FullStop},
|
||||
{CLKKey::ForwardSlash, Key::ForwardSlash},
|
||||
|
||||
{Key::Hyphen, 0x17},
|
||||
{Key::Equals, 0x18},
|
||||
{Key::Quote, 0x69},
|
||||
{CLKKey::Hyphen, Key::Hyphen},
|
||||
{CLKKey::Equals, Key::Caret},
|
||||
{CLKKey::BackTick, Key::Copy},
|
||||
|
||||
{Key::OpenSquareBracket, 0x38},
|
||||
{Key::CloseSquareBracket, 0x58},
|
||||
{Key::Semicolon, 0x48},
|
||||
{CLKKey::OpenSquareBracket, Key::OpenSquareBracket},
|
||||
{CLKKey::CloseSquareBracket, Key::CloseSquareBracket},
|
||||
|
||||
{Key::Enter, 0x49},
|
||||
{Key::Backspace, 0x59},
|
||||
{CLKKey::Semicolon, Key::Semicolon},
|
||||
{CLKKey::Quote, Key::Colon},
|
||||
|
||||
{Key::LeftShift, 0x00}, {Key::RightShift, 0x00},
|
||||
{Key::LeftControl, 0x01}, {Key::RightControl, 0x01},
|
||||
{CLKKey::Enter, Key::Return},
|
||||
{CLKKey::Backspace, Key::Delete},
|
||||
|
||||
{Key::Space, 0x62},
|
||||
{CLKKey::LeftShift, Key::Shift}, {CLKKey::RightShift, Key::Shift},
|
||||
{CLKKey::LeftControl, Key::Control}, {CLKKey::RightControl, Key::Control},
|
||||
{CLKKey::LeftOption, Key::CapsLock}, {CLKKey::RightOption, Key::CapsLock},
|
||||
|
||||
{CLKKey::Space, Key::Space},
|
||||
};
|
||||
};
|
||||
|
||||
struct CharacterMapper: public ::Utility::CharacterMapper {
|
||||
const uint16_t *sequence_for_character(const char character) const override {
|
||||
const auto found = sequences.find(character);
|
||||
return found != sequences.end() ? found->second.data() : nullptr;
|
||||
}
|
||||
|
||||
bool needs_pause_after_reset_all_keys() const override { return false; }
|
||||
bool needs_pause_after_key(const uint16_t key) const override {
|
||||
return !is_modifier(Key(key));
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr size_t MaxSequenceLength = 4;
|
||||
using Sequence = std::array<uint16_t, MaxSequenceLength>;
|
||||
|
||||
template <size_t n>
|
||||
requires (n < MaxSequenceLength - 1)
|
||||
static constexpr Sequence keys(const Key (&keys)[n]){
|
||||
Sequence sequence;
|
||||
for(size_t c = 0; c < n; c++) {
|
||||
sequence[c] = uint16_t(keys[c]);
|
||||
}
|
||||
sequence[n] = MachineTypes::MappedKeyboardMachine::KeyEndSequence;
|
||||
return sequence;
|
||||
}
|
||||
|
||||
static inline const std::unordered_map<char, Sequence> sequences = {
|
||||
{Utility::Typer::BeginString, keys({Key::SwitchOffCaps})},
|
||||
{Utility::Typer::EndString, keys({Key::RestoreCaps})},
|
||||
|
||||
{'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}) },
|
||||
{'4', keys({Key::k4}) }, {'5', keys({Key::k5}) },
|
||||
{'6', keys({Key::k6}) }, {'7', keys({Key::k7}) },
|
||||
{'8', keys({Key::k8}) }, {'9', keys({Key::k9}) },
|
||||
|
||||
{'\n', keys({Key::Return}) },
|
||||
{'\r', keys({Key::Return}) },
|
||||
{'\b', keys({Key::Delete}) },
|
||||
{'\t', keys({Key::Tab}) },
|
||||
{' ', keys({Key::Space}) },
|
||||
|
||||
{'!', keys({Key::Shift, Key::k1}) },
|
||||
{'"', keys({Key::Shift, Key::k2}) },
|
||||
{'#', keys({Key::Shift, Key::k3}) },
|
||||
{'$', keys({Key::Shift, Key::k4}) },
|
||||
{'%', keys({Key::Shift, Key::k5}) },
|
||||
{'&', keys({Key::Shift, Key::k6}) },
|
||||
{'\'', keys({Key::Shift, Key::k7}) },
|
||||
{'(', keys({Key::Shift, Key::k8}) },
|
||||
{')', keys({Key::Shift, Key::k9}) },
|
||||
|
||||
{'-', keys({Key::Hyphen}) },
|
||||
{'^', keys({Key::Caret}) },
|
||||
{'\\', keys({Key::Backslash}) },
|
||||
{'=', keys({Key::Shift, Key::Hyphen}) },
|
||||
{'~', keys({Key::Shift, Key::Caret}) },
|
||||
{'|', keys({Key::Shift, Key::Backslash}) },
|
||||
|
||||
{'@', keys({Key::At}) },
|
||||
{'[', keys({Key::OpenSquareBracket}) },
|
||||
{'_', keys({Key::Underscore}) },
|
||||
{'{', keys({Key::Shift, Key::OpenSquareBracket}) },
|
||||
// {'£', keys({BBCKey::Shift, BBCKey::Underscore}) },
|
||||
|
||||
{';', keys({Key::Semicolon}) },
|
||||
{':', keys({Key::Colon}) },
|
||||
{']', keys({Key::CloseSquareBracket}) },
|
||||
{'+', keys({Key::Shift, Key::Semicolon}) },
|
||||
{'*', keys({Key::Shift, Key::Colon}) },
|
||||
{'}', keys({Key::Shift, Key::CloseSquareBracket}) },
|
||||
|
||||
{',', keys({Key::Comma}) },
|
||||
{'.', keys({Key::FullStop}) },
|
||||
{'/', keys({Key::ForwardSlash}) },
|
||||
{'<', keys({Key::Shift, Key::Comma}) },
|
||||
{'>', keys({Key::Shift, Key::FullStop}) },
|
||||
{'?', keys({Key::Shift, Key::ForwardSlash}) },
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -778,7 +778,7 @@ private:
|
||||
bool speaker_is_enabled_ = false;
|
||||
|
||||
// MARK: - Caps Lock status and the activity observer.
|
||||
const std::string caps_led = "CAPS";
|
||||
static inline const std::string caps_led = "CAPS";
|
||||
bool caps_led_state_ = false;
|
||||
Activity::Observer *activity_observer_ = nullptr;
|
||||
};
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
using namespace Electron;
|
||||
|
||||
uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const {
|
||||
uint16_t KeyboardMapper::mapped_key_for_key(const Inputs::Keyboard::Key key) const {
|
||||
#define BIND(source, dest) case Inputs::Keyboard::Key::source: return Electron::Key::dest
|
||||
switch(key) {
|
||||
default: break;
|
||||
@@ -70,7 +70,7 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const {
|
||||
return MachineTypes::MappedKeyboardMachine::KeyNotMapped;
|
||||
}
|
||||
|
||||
const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
const uint16_t *CharacterMapper::sequence_for_character(const char character) const {
|
||||
#define KEYS(...) {__VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
|
||||
#define SHIFT(...) {KeyShift, __VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
|
||||
#define CTRL(...) {KeyControl, __VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
|
||||
@@ -148,6 +148,6 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
return table_lookup_sequence_for_character(key_sequences, character);
|
||||
}
|
||||
|
||||
bool CharacterMapper::needs_pause_after_key(uint16_t key) const {
|
||||
return key != KeyControl && key != KeyShift && key != KeyFunc;
|
||||
bool CharacterMapper::needs_pause_after_key(const uint16_t key) const {
|
||||
return !is_modifier(Key(key));
|
||||
}
|
||||
|
@@ -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 = 0xfffd,
|
||||
KeyF1 = 0xfe00, KeyF2, KeyF3, KeyF4, KeyF5, KeyF6, KeyF7, KeyF8, KeyF9, KeyF0,
|
||||
KeyBreak,
|
||||
};
|
||||
|
||||
constexpr bool is_modifier(const Key key) {
|
||||
|
@@ -36,6 +36,7 @@ void Plus3::set_control_register(uint8_t control) {
|
||||
// bit 1 => enable or disable drive 2
|
||||
// bit 2 => side select
|
||||
// bit 3 => single density select
|
||||
// bit 5 => reset?
|
||||
|
||||
uint8_t changes = control ^ last_control_;
|
||||
last_control_ = control;
|
||||
|
@@ -275,7 +275,7 @@ private:
|
||||
Mouse &mouse_;
|
||||
Joystick *joystick_ = nullptr;
|
||||
Activity::Observer *observer_ = nullptr;
|
||||
inline static const std::string led_name = "Power";
|
||||
static inline const std::string led_name = "Power";
|
||||
} cia_a_handler_;
|
||||
|
||||
class CIABHandler: public MOS::MOS6526::PortHandler {
|
||||
|
@@ -187,6 +187,71 @@ public:
|
||||
forceinline void perform_bus_cycle(const Motorola::CRTC::BusState &state) {
|
||||
// TODO: there's a one-tick delay on pixel output; incorporate that.
|
||||
|
||||
// For the interrupt timer: notify the leading edge of vertical sync and the
|
||||
// trailing edge of horizontal sync.
|
||||
if(previous_state_.vsync != state.vsync) {
|
||||
interrupt_timer_.set_vsync(state.vsync);
|
||||
}
|
||||
if(previous_state_.hsync && !state.hsync) {
|
||||
interrupt_timer_.signal_hsync();
|
||||
}
|
||||
|
||||
// Now, finally, process the previous state so as to incorporate a one-tick delay.
|
||||
output(previous_state_);
|
||||
previous_state_ = state;
|
||||
}
|
||||
|
||||
/// Sets the destination for output.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *const scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// @returns The current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 4.0f;
|
||||
}
|
||||
|
||||
/// Sets the type of display.
|
||||
void set_display_type(const Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
/// Gets the type of display.
|
||||
Outputs::Display::DisplayType get_display_type() const {
|
||||
return crt_.get_display_type();
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the next video mode. Per the documentation, mode changes take effect only at the end of line,
|
||||
not immediately. So next means "as of the end of this line".
|
||||
*/
|
||||
void set_next_mode(const int mode) {
|
||||
next_mode_ = mode;
|
||||
}
|
||||
|
||||
/// Palette management: selects a pen to modify.
|
||||
void select_pen(const int pen) {
|
||||
pen_ = pen;
|
||||
}
|
||||
|
||||
/// Palette management: sets the colour of the selected pen.
|
||||
void set_colour(const uint8_t colour) {
|
||||
if(pen_ & 16) {
|
||||
// If border is[/was] currently being output, flush what should have been
|
||||
// drawn in the old colour.
|
||||
if(previous_output_mode_ == OutputMode::Border) {
|
||||
output_border(cycles_);
|
||||
cycles_ = 0;
|
||||
}
|
||||
border_ = mapped_palette_value(colour);
|
||||
} else {
|
||||
palette_[pen_] = mapped_palette_value(colour);
|
||||
patch_mode_table(size_t(pen_));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void output(const Motorola::CRTC::BusState &state) {
|
||||
// The gate array waits 2us to react to the CRTC's vsync signal, and then
|
||||
// caps output at 4us. Since the clock rate is 1Mhz, that's 2 and 4 cycles,
|
||||
// respectively.
|
||||
@@ -251,9 +316,9 @@ public:
|
||||
// ... so form the real access address.
|
||||
const uint16_t address =
|
||||
uint16_t(
|
||||
((state.refresh_address & 0x3ff) << 1) |
|
||||
((state.row_address & 0x7) << 11) |
|
||||
((state.refresh_address & 0x3000) << 2)
|
||||
((state.refresh.get() & 0x3ff) << 1) |
|
||||
((state.line.get() & 0x7) << 11) |
|
||||
((state.refresh.get() & 0x3000) << 2)
|
||||
);
|
||||
|
||||
// Fetch two bytes and translate into pixels. Guaranteed: the mode can change only at
|
||||
@@ -308,71 +373,8 @@ public:
|
||||
}
|
||||
build_mode_table();
|
||||
}
|
||||
|
||||
// For the interrupt timer: notify the leading edge of vertical sync and the
|
||||
// trailing edge of horizontal sync.
|
||||
if(was_vsync_ != state.vsync) {
|
||||
interrupt_timer_.set_vsync(state.vsync);
|
||||
}
|
||||
if(was_hsync_ && !state.hsync) {
|
||||
interrupt_timer_.signal_hsync();
|
||||
}
|
||||
|
||||
// Update current state for edge detection next time around.
|
||||
was_vsync_ = state.vsync;
|
||||
was_hsync_ = state.hsync;
|
||||
}
|
||||
|
||||
/// Sets the destination for output.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *const scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// @returns The current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 4.0f;
|
||||
}
|
||||
|
||||
/// Sets the type of display.
|
||||
void set_display_type(const Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
/// Gets the type of display.
|
||||
Outputs::Display::DisplayType get_display_type() const {
|
||||
return crt_.get_display_type();
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the next video mode. Per the documentation, mode changes take effect only at the end of line,
|
||||
not immediately. So next means "as of the end of this line".
|
||||
*/
|
||||
void set_next_mode(const int mode) {
|
||||
next_mode_ = mode;
|
||||
}
|
||||
|
||||
/// Palette management: selects a pen to modify.
|
||||
void select_pen(const int pen) {
|
||||
pen_ = pen;
|
||||
}
|
||||
|
||||
/// Palette management: sets the colour of the selected pen.
|
||||
void set_colour(const uint8_t colour) {
|
||||
if(pen_ & 16) {
|
||||
// If border is[/was] currently being output, flush what should have been
|
||||
// drawn in the old colour.
|
||||
if(previous_output_mode_ == OutputMode::Border) {
|
||||
output_border(cycles_);
|
||||
cycles_ = 0;
|
||||
}
|
||||
border_ = mapped_palette_value(colour);
|
||||
} else {
|
||||
palette_[pen_] = mapped_palette_value(colour);
|
||||
patch_mode_table(size_t(pen_));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void output_border(const int length) {
|
||||
assert(length >= 0);
|
||||
|
||||
@@ -543,7 +545,6 @@ private:
|
||||
} previous_output_mode_ = OutputMode::Sync;
|
||||
int cycles_ = 0;
|
||||
|
||||
bool was_hsync_ = false, was_vsync_ = false;
|
||||
int cycles_into_hsync_ = 0;
|
||||
|
||||
Outputs::CRT::CRT crt_;
|
||||
@@ -568,6 +569,7 @@ private:
|
||||
uint8_t border_ = 0;
|
||||
|
||||
InterruptTimer &interrupt_timer_;
|
||||
Motorola::CRTC::BusState previous_state_;
|
||||
};
|
||||
using CRTC = Motorola::CRTC::CRTC6845<
|
||||
CRTCBusHandler,
|
||||
|
@@ -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 = 0xfffd,
|
||||
KeyRestore,
|
||||
};
|
||||
|
||||
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
|
@@ -137,6 +137,12 @@ public:
|
||||
*/
|
||||
static constexpr uint16_t KeyNotMapped = 0xfffe;
|
||||
|
||||
/*!
|
||||
Indicates that a key is not mapped (for the keyboard mapper) or that a
|
||||
character cannot be typed (for the character mapper).
|
||||
*/
|
||||
static constexpr uint16_t DelaySlot = 0xfffd;
|
||||
|
||||
/*!
|
||||
Allows individual machines to provide the mapping between host keys
|
||||
as per Inputs::Keyboard and their native scheme.
|
||||
|
@@ -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 = 0xfffd,
|
||||
KeyJasminReset = 0xfffc,
|
||||
KeyNMI = 0xfe00,
|
||||
KeyJasminReset,
|
||||
};
|
||||
|
||||
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
|
@@ -258,8 +258,8 @@ private:
|
||||
//
|
||||
// Meanwhile, row address is used as a substitute 14th address line.
|
||||
const auto base_address =
|
||||
((state.refresh_address & 0xfff) << 1) +
|
||||
((state.row_address & 1) << 13);
|
||||
((state.refresh.get() & 0xfff) << 1) +
|
||||
((state.line.get() & 1) << 13);
|
||||
const uint8_t bitmap[] = {
|
||||
ram[base_address],
|
||||
ram[base_address + 1],
|
||||
@@ -295,16 +295,16 @@ private:
|
||||
}
|
||||
|
||||
void serialise_text(const Motorola::CRTC::BusState &state) {
|
||||
const uint8_t attributes = ram[((state.refresh_address << 1) + 1) & 0x3fff];
|
||||
const uint8_t glyph = ram[((state.refresh_address << 1) + 0) & 0x3fff];
|
||||
const uint8_t row = font[(glyph * 8) + state.row_address];
|
||||
const uint8_t attributes = ram[((state.refresh.get() << 1) + 1) & 0x3fff];
|
||||
const uint8_t glyph = ram[((state.refresh.get() << 1) + 0) & 0x3fff];
|
||||
const uint8_t row = font[(glyph * 8) + state.line.get()];
|
||||
|
||||
uint8_t colours[2] = { rgb(attributes >> 4), rgbi(attributes) };
|
||||
|
||||
// Apply blink or background intensity.
|
||||
if(control_ & 0x20) {
|
||||
// Set both colours to black if within a blink; otherwise consider a yellow-to-brown conversion.
|
||||
if((attributes & 0x80) && (state.field_count & 16)) {
|
||||
if((attributes & 0x80) && state.field_count.bit<4>()) {
|
||||
colours[0] = colours[1] = 0;
|
||||
} else {
|
||||
colours[0] = yellow_to_brown(colours[0]);
|
||||
|
@@ -158,9 +158,9 @@ private:
|
||||
pixel_pointer[4] = pixel_pointer[5] = pixel_pointer[6] = pixel_pointer[7] =
|
||||
pixel_pointer[8] = low_intensity;
|
||||
} else {
|
||||
const uint8_t attributes = ram[((state.refresh_address << 1) + 1) & 0xfff];
|
||||
const uint8_t glyph = ram[((state.refresh_address << 1) + 0) & 0xfff];
|
||||
uint8_t row = font[(glyph * 14) + state.row_address];
|
||||
const uint8_t attributes = ram[((state.refresh.get() << 1) + 1) & 0xfff];
|
||||
const uint8_t glyph = ram[((state.refresh.get() << 1) + 0) & 0xfff];
|
||||
uint8_t row = font[(glyph * 14) + state.line.get()];
|
||||
|
||||
const uint8_t intensity = (attributes & 0x08) ? high_intensity : low_intensity;
|
||||
uint8_t blank = off;
|
||||
@@ -178,12 +178,12 @@ private:
|
||||
}
|
||||
|
||||
// Apply blink if enabled.
|
||||
if((control_ & 0x20) && (attributes & 0x80) && (state.field_count & 16)) {
|
||||
if((control_ & 0x20) && (attributes & 0x80) && state.field_count.bit<4>()) {
|
||||
row ^= 0xff;
|
||||
blank = (blank == off) ? intensity : off;
|
||||
}
|
||||
|
||||
if(((attributes & 7) == 1) && state.row_address == 13) {
|
||||
if(((attributes & 7) == 1) && state.line.get() == 13) {
|
||||
// Draw as underline.
|
||||
std::fill(pixel_pointer, pixel_pointer + 9, intensity);
|
||||
} else {
|
||||
|
@@ -1130,7 +1130,7 @@ using namespace PCCompatible;
|
||||
|
||||
namespace {
|
||||
#ifndef NDEBUG
|
||||
static constexpr bool ForceAT = true;
|
||||
static constexpr bool ForceAT = false;//true;
|
||||
#else
|
||||
static constexpr bool ForceAT = false;
|
||||
#endif
|
||||
|
@@ -429,6 +429,22 @@ const std::vector<Description> &Description::all_roms() {
|
||||
16_kb,
|
||||
0x3c14fc70u
|
||||
},
|
||||
{
|
||||
BBCMicroDFS226,
|
||||
"BBCMicro",
|
||||
"the Acorn 1770 DFS 2.26 ROM",
|
||||
"dfs-2.26.rom",
|
||||
16_kb,
|
||||
0x5ae33e94u
|
||||
},
|
||||
{
|
||||
BBCMicroADFS130,
|
||||
"BBCMicro",
|
||||
"the Acorn ADFS 1.30 ROM",
|
||||
"adfs-1.30.rom",
|
||||
16_kb,
|
||||
0xd3855588u
|
||||
},
|
||||
|
||||
//
|
||||
// ColecoVision.
|
||||
|
@@ -82,6 +82,8 @@ enum Name {
|
||||
|
||||
// BBC Micro.
|
||||
BBCMicroMOS12,
|
||||
BBCMicroDFS226,
|
||||
BBCMicroADFS130,
|
||||
|
||||
// ColecoVision.
|
||||
ColecoVisionBIOS,
|
||||
|
@@ -10,7 +10,13 @@
|
||||
|
||||
using namespace Utility;
|
||||
|
||||
Typer::Typer(const std::string &string, HalfCycles delay, HalfCycles frequency, CharacterMapper &character_mapper, Delegate *delegate) :
|
||||
Typer::Typer(
|
||||
const std::string &string,
|
||||
const HalfCycles delay,
|
||||
const HalfCycles frequency,
|
||||
CharacterMapper &character_mapper,
|
||||
Delegate *const delegate
|
||||
) :
|
||||
frequency_(frequency),
|
||||
counter_(-delay),
|
||||
delegate_(delegate),
|
||||
@@ -70,7 +76,7 @@ void Typer::append(const std::string &string) {
|
||||
}
|
||||
}
|
||||
|
||||
const uint16_t *Typer::sequence_for_character(char c) const {
|
||||
const uint16_t *Typer::sequence_for_character(const char c) const {
|
||||
const uint16_t *const sequence = character_mapper_.sequence_for_character(c);
|
||||
if(!sequence || sequence[0] == MachineTypes::MappedKeyboardMachine::KeyNotMapped) {
|
||||
return nullptr;
|
||||
@@ -96,20 +102,17 @@ uint16_t Typer::try_type_next_character() {
|
||||
delegate_->clear_all_keys();
|
||||
if(character_mapper_.needs_pause_after_reset_all_keys() ||
|
||||
(string_pointer_ > 0 && string_[string_pointer_ - 1] == string_[string_pointer_])) {
|
||||
return 0xffff; // Arbitrarily. Anything non-zero will do.
|
||||
return MachineTypes::MappedKeyboardMachine::DelaySlot;
|
||||
}
|
||||
++phase_;
|
||||
}
|
||||
|
||||
// If the sequence is over, stop.
|
||||
if(sequence[phase_ - 2] == MachineTypes::MappedKeyboardMachine::KeyEndSequence) {
|
||||
return 0;
|
||||
// Don't forward ::KeyEndSequence.
|
||||
const auto next = sequence[phase_ - 2];
|
||||
if(next != MachineTypes::MappedKeyboardMachine::KeyEndSequence) {
|
||||
delegate_->set_key_state(sequence[phase_ - 2], true);
|
||||
}
|
||||
|
||||
// Otherwise, type the key.
|
||||
delegate_->set_key_state(sequence[phase_ - 2], true);
|
||||
|
||||
return sequence[phase_ - 2];
|
||||
return next;
|
||||
}
|
||||
|
||||
bool Typer::type_next_character() {
|
||||
@@ -118,13 +121,14 @@ bool Typer::type_next_character() {
|
||||
while(true) {
|
||||
const uint16_t key_pressed = try_type_next_character();
|
||||
|
||||
if(!key_pressed) {
|
||||
if(key_pressed == MachineTypes::MappedKeyboardMachine::KeyEndSequence) {
|
||||
phase_ = 0;
|
||||
++string_pointer_;
|
||||
if(string_pointer_ == string_.size()) return false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(key_pressed && character_mapper_.needs_pause_after_key(key_pressed)) {
|
||||
if(character_mapper_.needs_pause_after_key(key_pressed)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@@ -82,8 +82,8 @@ public:
|
||||
/// Adds the contents of @c str to the end of the current string.
|
||||
void append(const std::string &str);
|
||||
|
||||
const char BeginString = 0x02; // i.e. ASCII start of text
|
||||
const char EndString = 0x03; // i.e. ASCII end of text
|
||||
static constexpr char BeginString = 0x02; // i.e. ASCII start of text.
|
||||
static constexpr char EndString = 0x03; // i.e. ASCII end of text.
|
||||
|
||||
private:
|
||||
std::string string_;
|
||||
|
@@ -23,7 +23,9 @@ template <typename Full, typename Half> union alignas(Full) alignas(Half) Regist
|
||||
RegisterPair() = default;
|
||||
|
||||
Full full;
|
||||
auto operator <=>(const RegisterPair &rhs) const = default;
|
||||
auto operator <=>(const RegisterPair &rhs) const {
|
||||
return full <=> rhs.full;
|
||||
}
|
||||
#if TARGET_RT_BIG_ENDIAN
|
||||
struct {
|
||||
Half high, low;
|
||||
|
106
Numeric/SizedInt.hpp
Normal file
106
Numeric/SizedInt.hpp
Normal file
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// SizedCounter.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 22/09/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Sizes.hpp"
|
||||
|
||||
namespace Numeric {
|
||||
|
||||
/*!
|
||||
Provides a counter that is strictly limited to the requested of bits but attempts otherwise
|
||||
to act like a standard C++ numeric type.
|
||||
*/
|
||||
template <int bits>
|
||||
struct SizedInt {
|
||||
using IntT = MinIntForValue<1 << bits>::type;
|
||||
inline static constexpr IntT Mask = (1 << bits) - 1;
|
||||
|
||||
constexpr SizedInt(const IntT start_value) noexcept : counter_(start_value & Mask) {}
|
||||
SizedInt() = default;
|
||||
|
||||
IntT get() const {
|
||||
return counter_;
|
||||
}
|
||||
|
||||
SizedInt operator +(const SizedInt offset) const { return SizedInt<bits>(counter_ + offset.counter_); }
|
||||
SizedInt operator -(const SizedInt offset) const { return SizedInt<bits>(counter_ - offset.counter_); }
|
||||
SizedInt operator &(const SizedInt offset) const { return SizedInt<bits>(counter_ & offset.counter_); }
|
||||
SizedInt operator |(const SizedInt offset) const { return SizedInt<bits>(counter_ | offset.counter_); }
|
||||
SizedInt operator ^(const SizedInt offset) const { return SizedInt<bits>(counter_ ^ offset.counter_); }
|
||||
SizedInt operator >>(const int shift) const { return SizedInt<bits>(counter_ >> shift); }
|
||||
SizedInt operator <<(const int shift) const { return SizedInt<bits>(counter_ << shift); }
|
||||
|
||||
SizedInt &operator &=(const SizedInt offset) {
|
||||
counter_ &= offset.counter_;
|
||||
return *this;
|
||||
}
|
||||
SizedInt &operator |=(const SizedInt offset) {
|
||||
counter_ |= offset.counter_;
|
||||
return *this;
|
||||
}
|
||||
SizedInt &operator ^=(const SizedInt offset) {
|
||||
counter_ ^= offset.counter_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SizedInt &operator <<=(const int shift) {
|
||||
counter_ = (counter_ << shift) & Mask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SizedInt &operator >>=(const int shift) {
|
||||
counter_ >>= shift;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SizedInt &operator ++(int) {
|
||||
++(*this);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SizedInt &operator ++() {
|
||||
counter_ = (counter_ + 1) & Mask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SizedInt &operator +=(const IntT rhs) {
|
||||
counter_ = (counter_ + rhs) & Mask;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool operator!() const {
|
||||
return !counter_;
|
||||
}
|
||||
|
||||
auto operator <=>(const SizedInt &) const = default;
|
||||
|
||||
/// Replaces the bits in the range [begin, end) with those in the low-order bits of @c vlaue.
|
||||
template <int begin, int end>
|
||||
void load(const MinIntForValue<1 << (end - begin)>::type value) {
|
||||
const auto mask = (1 << end) - (1 << begin);
|
||||
counter_ &= ~mask;
|
||||
counter_ |= IntT((value << begin) & mask);
|
||||
}
|
||||
|
||||
template <int begin, typename IntT>
|
||||
void load(const IntT value) {
|
||||
load<begin, begin + sizeof(IntT)*8>(value);
|
||||
}
|
||||
|
||||
template <int index>
|
||||
requires (index < bits)
|
||||
bool bit() const {
|
||||
return counter_ & (1 << index);
|
||||
}
|
||||
|
||||
private:
|
||||
IntT counter_{};
|
||||
};
|
||||
|
||||
}
|
@@ -360,6 +360,9 @@
|
||||
4B4518A51F75FD1C00926311 /* SSD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4518991F75FD1B00926311 /* SSD.cpp */; };
|
||||
4B47770B268FBE4D005C2340 /* FAT.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B477709268FBE4D005C2340 /* FAT.cpp */; };
|
||||
4B47770D26900685005C2340 /* EnterpriseDaveTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B47770C26900685005C2340 /* EnterpriseDaveTests.mm */; };
|
||||
4B47F3B62E7BAB94005D4DEC /* uPD7002.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F3B52E7BAB94005D4DEC /* uPD7002.cpp */; };
|
||||
4B47F3B72E7BAB94005D4DEC /* uPD7002.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F3B52E7BAB94005D4DEC /* uPD7002.cpp */; };
|
||||
4B47F3B82E7BAB94005D4DEC /* uPD7002.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F3B52E7BAB94005D4DEC /* uPD7002.cpp */; };
|
||||
4B47F6C5241C87A100ED06F7 /* Struct.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F6C4241C87A100ED06F7 /* Struct.cpp */; };
|
||||
4B47F6C6241C87A100ED06F7 /* Struct.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B47F6C4241C87A100ED06F7 /* Struct.cpp */; };
|
||||
4B49F0A923346F7A0045E6A6 /* MacintoshOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B49F0A723346F7A0045E6A6 /* MacintoshOptions.xib */; };
|
||||
@@ -638,6 +641,9 @@
|
||||
4B882F5B2C2F9C7700D84031 /* Shaker in Resources */ = {isa = PBXBuildFile; fileRef = 4B882F5A2C2F9C7700D84031 /* Shaker */; };
|
||||
4B882F5C2C32199400D84031 /* MachineForTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B055ABE1FAE98000060FFFF /* MachineForTarget.cpp */; };
|
||||
4B882F5D2C3219A400D84031 /* AmstradCPC.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B38F3461F2EC11D00D9235D /* AmstradCPC.cpp */; };
|
||||
4B8855A52E84D51B00E251DD /* SAA5050.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8855A32E84D51B00E251DD /* SAA5050.cpp */; };
|
||||
4B8855A62E84D51B00E251DD /* SAA5050.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8855A32E84D51B00E251DD /* SAA5050.cpp */; };
|
||||
4B8855A72E84D51B00E251DD /* SAA5050.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8855A32E84D51B00E251DD /* SAA5050.cpp */; };
|
||||
4B89449520194CB3007DE474 /* MachineForTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B055ABE1FAE98000060FFFF /* MachineForTarget.cpp */; };
|
||||
4B894518201967B4007DE474 /* ConfidenceCounter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8944E6201967B4007DE474 /* ConfidenceCounter.cpp */; };
|
||||
4B894519201967B4007DE474 /* ConfidenceCounter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8944E6201967B4007DE474 /* ConfidenceCounter.cpp */; };
|
||||
@@ -1597,6 +1603,8 @@
|
||||
4B477709268FBE4D005C2340 /* FAT.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = FAT.cpp; path = Parsers/FAT.cpp; sourceTree = "<group>"; };
|
||||
4B47770A268FBE4D005C2340 /* FAT.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = FAT.hpp; path = Parsers/FAT.hpp; sourceTree = "<group>"; };
|
||||
4B47770C26900685005C2340 /* EnterpriseDaveTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EnterpriseDaveTests.mm; sourceTree = "<group>"; };
|
||||
4B47F3B32E7B9A14005D4DEC /* uPD7002.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = uPD7002.hpp; sourceTree = "<group>"; };
|
||||
4B47F3B52E7BAB94005D4DEC /* uPD7002.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = uPD7002.cpp; sourceTree = "<group>"; };
|
||||
4B47F6C4241C87A100ED06F7 /* Struct.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Struct.cpp; sourceTree = "<group>"; };
|
||||
4B49F0A823346F7A0045E6A6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/MacintoshOptions.xib"; sourceTree = SOURCE_ROOT; };
|
||||
4B4A762E1DB1A3FA007AAE2E /* AY38910.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AY38910.cpp; sourceTree = "<group>"; };
|
||||
@@ -1789,6 +1797,9 @@
|
||||
4B8805FA1DCFF807003085B1 /* Oric.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Oric.hpp; path = Parsers/Oric.hpp; sourceTree = "<group>"; };
|
||||
4B882F582C2F9C6900D84031 /* CPCShakerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CPCShakerTests.mm; sourceTree = "<group>"; };
|
||||
4B882F5A2C2F9C7700D84031 /* Shaker */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Shaker; sourceTree = "<group>"; };
|
||||
4B88559C2E8185BF00E251DD /* SizedInt.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SizedInt.hpp; sourceTree = "<group>"; };
|
||||
4B8855A22E84D51B00E251DD /* SAA5050.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SAA5050.hpp; sourceTree = "<group>"; };
|
||||
4B8855A32E84D51B00E251DD /* SAA5050.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SAA5050.cpp; sourceTree = "<group>"; };
|
||||
4B89449220194A47007DE474 /* CSStaticAnalyser+TargetVector.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "CSStaticAnalyser+TargetVector.h"; path = "StaticAnalyser/CSStaticAnalyser+TargetVector.h"; sourceTree = "<group>"; };
|
||||
4B8944E4201967B4007DE474 /* ConfidenceSummary.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ConfidenceSummary.hpp; sourceTree = "<group>"; };
|
||||
4B8944E5201967B4007DE474 /* ConfidenceSource.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ConfidenceSource.hpp; sourceTree = "<group>"; };
|
||||
@@ -3316,6 +3327,15 @@
|
||||
path = Formats;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4B47F3B42E7B9A14005D4DEC /* uPD7002 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4B47F3B52E7BAB94005D4DEC /* uPD7002.cpp */,
|
||||
4B47F3B32E7B9A14005D4DEC /* uPD7002.hpp */,
|
||||
);
|
||||
path = uPD7002;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4B4A762D1DB1A35C007AAE2E /* AY38910 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -3727,15 +3747,16 @@
|
||||
children = (
|
||||
4B43984129674943006B0BFC /* BitReverse.hpp */,
|
||||
4BD155312716362A00410C6E /* BitSpread.hpp */,
|
||||
4B0B239F2D658C9400153879 /* BitStream.hpp */,
|
||||
4281572E2AA0334300E16AA1 /* Carry.hpp */,
|
||||
4B7BA03E23D55E7900B98D9E /* CRC.hpp */,
|
||||
4B7BA03F23D55E7900B98D9E /* LFSR.hpp */,
|
||||
4B66E1A8297719270057ED0F /* NumericCoder.hpp */,
|
||||
4BB5B995281B1D3E00522DA9 /* RegisterSizes.hpp */,
|
||||
4B88559C2E8185BF00E251DD /* SizedInt.hpp */,
|
||||
4BFEA2F12682A90200EBF94C /* Sizes.hpp */,
|
||||
4BD9713A2BFD7E7100C907AA /* StringSimilarity.hpp */,
|
||||
4B0329202D0A78DC00C51EB5 /* UpperBound.hpp */,
|
||||
4B0B239F2D658C9400153879 /* BitStream.hpp */,
|
||||
);
|
||||
name = Numeric;
|
||||
path = ../../Numeric;
|
||||
@@ -3831,6 +3852,15 @@
|
||||
path = Data;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4B8855A42E84D51B00E251DD /* SAA5050 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4B8855A22E84D51B00E251DD /* SAA5050.hpp */,
|
||||
4B8855A32E84D51B00E251DD /* SAA5050.cpp */,
|
||||
);
|
||||
path = SAA5050;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4B8944E2201967B4007DE474 /* Analyser */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -5073,8 +5103,10 @@
|
||||
4B4B1A39200198C900A0F866 /* KonamiSCC */,
|
||||
4BC23A212467600E001A6030 /* OPx */,
|
||||
4BF0BC6E2973318E00CCA2B5 /* RP5C01 */,
|
||||
4B8855A42E84D51B00E251DD /* SAA5050 */,
|
||||
4B0ACBFF237756EC008902D0 /* Serial */,
|
||||
4BB0A6582044FD3000FB3688 /* SN76489 */,
|
||||
4B47F3B42E7B9A14005D4DEC /* uPD7002 */,
|
||||
);
|
||||
name = Components;
|
||||
path = ../../Components;
|
||||
@@ -6064,6 +6096,7 @@
|
||||
4BB505832B962DDF0031C43C /* Keyboard.cpp in Sources */,
|
||||
4BF0BC69297108D600CCA2B5 /* MemorySlotHandler.cpp in Sources */,
|
||||
4B894521201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
|
||||
4B47F3B62E7BAB94005D4DEC /* uPD7002.cpp in Sources */,
|
||||
4B8318B522D3E548006DB630 /* Macintosh.cpp in Sources */,
|
||||
4B8DF4FA254E36AE00F3433C /* Video.cpp in Sources */,
|
||||
4B7BA03123C2B19C00B98D9E /* Jasmin.cpp in Sources */,
|
||||
@@ -6283,6 +6316,7 @@
|
||||
4B302185208A550100773308 /* DiskII.cpp in Sources */,
|
||||
42EB81292B23AAC300429AF4 /* IMD.cpp in Sources */,
|
||||
4B051CB1267C1CA200CA44E8 /* Keyboard.cpp in Sources */,
|
||||
4B8855A72E84D51B00E251DD /* SAA5050.cpp in Sources */,
|
||||
4B0F1BB32602645900B85C66 /* StaticAnalyser.cpp in Sources */,
|
||||
4B055A931FAE85B50060FFFF /* BinaryDump.cpp in Sources */,
|
||||
4B89452D201967B4007DE474 /* Tape.cpp in Sources */,
|
||||
@@ -6379,6 +6413,7 @@
|
||||
4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */,
|
||||
4BE8EB6625C750B50040BC40 /* DAT.cpp in Sources */,
|
||||
4BBFFEE61F7B27F1005F3FEB /* TrackSerialiser.cpp in Sources */,
|
||||
4B8855A52E84D51B00E251DD /* SAA5050.cpp in Sources */,
|
||||
4BAE49582032881E004BE78E /* CSZX8081.mm in Sources */,
|
||||
4B0333AF2094081A0050B93D /* AppleDSK.cpp in Sources */,
|
||||
4B894518201967B4007DE474 /* ConfidenceCounter.cpp in Sources */,
|
||||
@@ -6434,6 +6469,7 @@
|
||||
4B89452C201967B4007DE474 /* Tape.cpp in Sources */,
|
||||
4B448E811F1C45A00009ABD6 /* TZX.cpp in Sources */,
|
||||
4BBFE83D21015D9C00BF1C40 /* CSJoystickManager.m in Sources */,
|
||||
4B47F3B72E7BAB94005D4DEC /* uPD7002.cpp in Sources */,
|
||||
4BEBFB512002DB30000708CC /* DiskROM.cpp in Sources */,
|
||||
4B89451C201967B4007DE474 /* Disk.cpp in Sources */,
|
||||
4B302184208A550100773308 /* DiskII.cpp in Sources */,
|
||||
@@ -6609,6 +6645,7 @@
|
||||
4B778F3123A5F0CB0000D260 /* Keyboard.cpp in Sources */,
|
||||
4B778F4A23A5F1FB0000D260 /* StaticAnalyser.cpp in Sources */,
|
||||
4B7752AB28217E560073E2C5 /* SZX.cpp in Sources */,
|
||||
4B8855A62E84D51B00E251DD /* SAA5050.cpp in Sources */,
|
||||
4BD91D772401C2B8007BDC91 /* PatrikRakTests.swift in Sources */,
|
||||
4B06AAE22C645F970034D014 /* PCBooter.cpp in Sources */,
|
||||
4B1082C32C1A87CA00B07C5D /* CSL.cpp in Sources */,
|
||||
@@ -6792,6 +6829,7 @@
|
||||
4BDA8235261E8E000021AA19 /* Z80ContentionTests.mm in Sources */,
|
||||
4B882F592C2F9C6A00D84031 /* CPCShakerTests.mm in Sources */,
|
||||
4B7752C328217F720073E2C5 /* Z80.cpp in Sources */,
|
||||
4B47F3B82E7BAB94005D4DEC /* uPD7002.cpp in Sources */,
|
||||
4B06AACE2C645EEC0034D014 /* PCCompatible.cpp in Sources */,
|
||||
4B06AB072C6461160034D014 /* StaticAnalyser.cpp in Sources */,
|
||||
4B778F1A23A5ED320000D260 /* Video.cpp in Sources */,
|
||||
|
@@ -67,7 +67,6 @@
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableASanStackUseAfterReturn = "YES"
|
||||
disableMainThreadChecker = "YES"
|
||||
disablePerformanceAntipatternChecker = "YES"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@@ -75,7 +74,8 @@
|
||||
migratedStopOnEveryIssue = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "NO">
|
||||
allowLocationSimulation = "NO"
|
||||
disablePerformanceAntipatternChecker = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
|
@@ -821,11 +821,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>25.09.14</string>
|
||||
<string>25.10.03</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>25.09.14</string>
|
||||
<string>25.10.03</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.entertainment</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
@@ -164,14 +164,9 @@
|
||||
self = [super init];
|
||||
if(self) {
|
||||
auto target = std::make_unique<Analyser::Static::Acorn::BBCMicroTarget>();
|
||||
// TODO.
|
||||
(void)dfs;
|
||||
(void)adfs;
|
||||
(void)sidewaysRAM;
|
||||
// target->has_dfs = dfs;
|
||||
// target->has_pres_adfs = adfs;
|
||||
// target->has_ap6_rom = ap6;
|
||||
// target->has_sideways_ram = sidewaysRAM;
|
||||
target->has_1770dfs = dfs;
|
||||
target->has_adfs = adfs;
|
||||
target->has_sideways_ram = sidewaysRAM;
|
||||
_targets.push_back(std::move(target));
|
||||
}
|
||||
return self;
|
||||
|
@@ -95,12 +95,9 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
|
||||
func establishStoredOptions() {
|
||||
let standardUserDefaults = UserDefaults.standard
|
||||
|
||||
// Set up data soure.
|
||||
|
||||
// TEMPORARY: remove the Apple IIgs and PC compatible options.
|
||||
// Neither is yet a fully-working machine.
|
||||
#if !DEBUG
|
||||
for hidden in ["appleiigs", "bbcmicro"] {
|
||||
// Remove options that are not yet fully working, except in debug builds.
|
||||
for hidden in ["appleiigs"] {
|
||||
let tabIndex = machineSelector.indexOfTabViewItem(withIdentifier: hidden)
|
||||
machineSelector.removeTabViewItem(machineSelector.tabViewItem(at: tabIndex))
|
||||
}
|
||||
|
@@ -76,8 +76,10 @@ SOURCES += \
|
||||
$$SRC/Components/KonamiSCC/*.cpp \
|
||||
$$SRC/Components/OPx/*.cpp \
|
||||
$$SRC/Components/RP5C01/*.cpp \
|
||||
$$SRC/Components/SAA5050/*.cpp \
|
||||
$$SRC/Components/Serial/*.cpp \
|
||||
$$SRC/Components/SN76489/*.cpp \
|
||||
$$SRC/Components/uPD7002/*.cpp \
|
||||
\
|
||||
$$SRC/Inputs/*.cpp \
|
||||
\
|
||||
@@ -211,8 +213,10 @@ HEADERS += \
|
||||
$$SRC/Components/OPx/*.hpp \
|
||||
$$SRC/Components/OPx/Implementation/*.hpp \
|
||||
$$SRC/Components/RP5C01/*.hpp \
|
||||
$$SRC/Components/SAA5050/*.hpp \
|
||||
$$SRC/Components/Serial/*.hpp \
|
||||
$$SRC/Components/SN76489/*.hpp \
|
||||
$$SRC/Components/uPD7002/*.hpp \
|
||||
\
|
||||
$$SRC/Concurrency/*.hpp \
|
||||
\
|
||||
|
@@ -60,8 +60,10 @@ SOURCES += glob.glob('../../Components/I2C/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/KonamiSCC/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/OPx/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/RP5C01/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/SAA5050/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/SN76489/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/Serial/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/uPD7002/*.cpp')
|
||||
|
||||
SOURCES += glob.glob('../../Configurable/*.cpp')
|
||||
|
||||
|
@@ -194,13 +194,10 @@ void Processor<personality, T, uses_ready_line>::run_for(const Cycles cycles) {
|
||||
case OperationRSTPickVector: next_address_.full = 0xfffc; continue;
|
||||
case CycleReadVectorLow: read_mem(pc_.halves.low, next_address_.full); break;
|
||||
case CycleReadVectorHigh: read_mem(pc_.halves.high, next_address_.full+1); break;
|
||||
case OperationSetIRQFlags:
|
||||
case OperationSetInterruptFlags:
|
||||
flags_.inverse_interrupt = 0;
|
||||
if(is_65c02(personality)) flags_.decimal = 0;
|
||||
continue;
|
||||
case OperationSetNMIRSTFlags:
|
||||
if(is_65c02(personality)) flags_.decimal = 0;
|
||||
continue;
|
||||
|
||||
case CyclePullPCL: s_++; read_mem(pc_.halves.low, s_ | 0x100); break;
|
||||
case CyclePullPCH: s_++; read_mem(pc_.halves.high, s_ | 0x100); break;
|
||||
|
@@ -77,7 +77,7 @@ using namespace CPU::MOS6502;
|
||||
|
||||
ProcessorStorage::ProcessorStorage(Personality personality) {
|
||||
const InstructionList operations_6502[] = {
|
||||
/* 0x00 BRK */ Program(CycleIncPCPushPCH, CyclePushPCL, OperationBRKPickVector, OperationSetOperandFromFlagsWithBRKSet, CyclePushOperand, OperationSetIRQFlags, CycleReadVectorLow, CycleReadVectorHigh),
|
||||
/* 0x00 BRK */ Program(CycleIncPCPushPCH, CyclePushPCL, OperationBRKPickVector, OperationSetOperandFromFlagsWithBRKSet, CyclePushOperand, OperationSetInterruptFlags, CycleReadVectorLow, CycleReadVectorHigh),
|
||||
/* 0x01 ORA x, ind */ IndexedIndirectRead(OperationORA),
|
||||
/* 0x02 JAM */ JAM, /* 0x03 ASO x, ind */ IndexedIndirectReadModifyWrite(OperationASO),
|
||||
/* 0x04 NOP zpg */ ZeroNop(), /* 0x05 ORA zpg */ ZeroRead(OperationORA),
|
||||
@@ -228,7 +228,7 @@ ProcessorStorage::ProcessorStorage(Personality personality) {
|
||||
CycleNoWritePush,
|
||||
OperationRSTPickVector,
|
||||
CycleNoWritePush,
|
||||
OperationSetNMIRSTFlags,
|
||||
OperationSetInterruptFlags,
|
||||
CycleReadVectorLow,
|
||||
CycleReadVectorHigh
|
||||
),
|
||||
@@ -242,7 +242,7 @@ ProcessorStorage::ProcessorStorage(Personality personality) {
|
||||
OperationBRKPickVector,
|
||||
OperationSetOperandFromFlags,
|
||||
CyclePushOperand,
|
||||
OperationSetIRQFlags,
|
||||
OperationSetInterruptFlags,
|
||||
CycleReadVectorLow,
|
||||
CycleReadVectorHigh
|
||||
),
|
||||
@@ -256,7 +256,7 @@ ProcessorStorage::ProcessorStorage(Personality personality) {
|
||||
OperationNMIPickVector,
|
||||
OperationSetOperandFromFlags,
|
||||
CyclePushOperand,
|
||||
OperationSetNMIRSTFlags,
|
||||
OperationSetInterruptFlags,
|
||||
CycleReadVectorLow,
|
||||
CycleReadVectorHigh
|
||||
),
|
||||
|
@@ -35,8 +35,7 @@ class ProcessorStorage {
|
||||
CyclePushY, // pushes Y to the stack
|
||||
CyclePushOperand, // pushes operand_ to the stack
|
||||
|
||||
OperationSetIRQFlags, // 6502: sets I; 65C02: sets I and resets D
|
||||
OperationSetNMIRSTFlags, // 6502: no-op. 65C02: resets D
|
||||
OperationSetInterruptFlags, // 6502: sets I; 65C02: sets I and resets D
|
||||
|
||||
OperationBRKPickVector, // 65C02: sets next_address_ to the BRK vector location; 6502: as 65C02 if no NMI is pending; otherwise sets next_address_ to the NMI address and resets the internal NMI-pending flag
|
||||
OperationNMIPickVector, // sets next_address_ to the NMI vector
|
||||
|
@@ -17,6 +17,7 @@ It currently contains emulations of the:
|
||||
* Apple II/II+ and IIe;
|
||||
* Atari 2600;
|
||||
* Atari ST;
|
||||
* BBC Micro;
|
||||
* ColecoVision;
|
||||
* Commodore Vic-20;
|
||||
* Enterprise 64/128;
|
||||
@@ -133,8 +134,8 @@ This emulator attempts cycle-accurate emulation of all supported machines. In so
|
||||
<td width=50%><img alt="Amiga James Pond II" src="READMEImages/AmigaJamesPondII.png"></td>
|
||||
<td width=50%><img alt="Atari 2600 Solaris" src="READMEImages/Atari2600Solaris.png"></td>
|
||||
</tr><tr>
|
||||
<td width=50%><img alt="Enterprise HERO" src="READMEImages/EnterpriseHERO.png"></td>
|
||||
<td width=50%><img alt="Microsoft Flight Simulator" src="READMEImages/PCFlightSimulator.png"></td>
|
||||
<td width=50%><img alt="BBC Micro Elite" src="READMEImages/BBCElite.png"></td>
|
||||
</tr><tr>
|
||||
<td width=50%><img alt="ColecoVision Galaxian" src="READMEImages/ColecoVisionGalaxian.png"></td>
|
||||
<td width=50%><img alt="SG1000 Chack'n'Pop" src="READMEImages/SGChackNPop.png"></td>
|
||||
|
BIN
READMEImages/BBCElite.png
Normal file
BIN
READMEImages/BBCElite.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 162 KiB |
@@ -3,3 +3,5 @@ ROM files would ordinarily go here; the copyright status of these is uncertain s
|
||||
Expected files:
|
||||
|
||||
os12.rom
|
||||
dfs-2.26.rom
|
||||
adfs-1.30.rom
|
||||
|
@@ -58,8 +58,10 @@ set(CLK_SOURCES
|
||||
Components/KonamiSCC/KonamiSCC.cpp
|
||||
Components/OPx/OPLL.cpp
|
||||
Components/RP5C01/RP5C01.cpp
|
||||
Components/SAA5050/SAA5050.cpp
|
||||
Components/SN76489/SN76489.cpp
|
||||
Components/Serial/Line.cpp
|
||||
Components/uPD7002/uPD7002.cpp
|
||||
|
||||
Inputs/Keyboard.cpp
|
||||
|
||||
|
Reference in New Issue
Block a user