diff --git a/Analyser/Machines.hpp b/Analyser/Machines.hpp index 2f36858bf..7159408f8 100644 --- a/Analyser/Machines.hpp +++ b/Analyser/Machines.hpp @@ -24,7 +24,8 @@ enum class Machine { MSX, Oric, Vic20, - ZX8081 + ZX8081, + ZXSpectrum, }; } diff --git a/Analyser/Static/AmstradCPC/StaticAnalyser.cpp b/Analyser/Static/AmstradCPC/StaticAnalyser.cpp index 500e3ce55..2ca6924b6 100644 --- a/Analyser/Static/AmstradCPC/StaticAnalyser.cpp +++ b/Analyser/Static/AmstradCPC/StaticAnalyser.cpp @@ -11,12 +11,12 @@ #include #include -#include "Target.hpp" - #include "../../../Storage/Disk/Parsers/CPM.hpp" #include "../../../Storage/Disk/Encodings/MFM/Parser.hpp" #include "../../../Storage/Tape/Parsers/Spectrum.hpp" +#include "Target.hpp" + namespace { bool strcmp_insensitive(const char *a, const char *b) { diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp index 2b3cf18af..7fe020af2 100644 --- a/Analyser/Static/StaticAnalyser.cpp +++ b/Analyser/Static/StaticAnalyser.cpp @@ -28,6 +28,7 @@ #include "Oric/StaticAnalyser.hpp" #include "Sega/StaticAnalyser.hpp" #include "ZX8081/StaticAnalyser.hpp" +#include "ZXSpectrum/StaticAnalyser.hpp" // Cartridges #include "../../Storage/Cartridge/Formats/BinaryDump.hpp" @@ -65,6 +66,7 @@ #include "../../Storage/Tape/Formats/TapeUEF.hpp" #include "../../Storage/Tape/Formats/TZX.hpp" #include "../../Storage/Tape/Formats/ZX80O81P.hpp" +#include "../../Storage/Tape/Formats/ZXSpectrumTAP.hpp" // Target Platform Types #include "../../Storage/TargetPlatforms.hpp" @@ -116,7 +118,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge) // BIN (cartridge dump) Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX) // CAS Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC) // CDT - Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::ColecoVision) // COL + Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Coleco) // COL Format("csw", result.tapes, Tape::CSW, TargetPlatform::AllTape) // CSW Format("d64", result.disks, Disk::DiskImageHolder, TargetPlatform::Commodore) // D64 Format("dat", result.mass_storage_devices, MassStorage::DAT, TargetPlatform::Acorn) // DAT @@ -126,7 +128,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: Format( "dsk", result.disks, Disk::DiskImageHolder, - TargetPlatform::AmstradCPC | TargetPlatform::Oric) // DSK (Amstrad CPC) + TargetPlatform::AmstradCPC | TargetPlatform::Oric | TargetPlatform::ZXSpectrum) // DSK (Amstrad CPC, etc) Format("dsk", result.disks, Disk::DiskImageHolder, TargetPlatform::DiskII) // DSK (Apple II) Format("dsk", result.disks, Disk::DiskImageHolder, TargetPlatform::Macintosh) // DSK (Macintosh, floppy disk) Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk) @@ -168,7 +170,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: Format( "rom", result.cartridges, Cartridge::BinaryDump, - TargetPlatform::AcornElectron | TargetPlatform::ColecoVision | TargetPlatform::MSX) // ROM + TargetPlatform::AcornElectron | TargetPlatform::Coleco | TargetPlatform::MSX) // ROM Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SG Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS Format("ssd", result.disks, Disk::DiskImageHolder, TargetPlatform::Acorn) // SSD @@ -176,8 +178,9 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: Format("stx", result.disks, Disk::DiskImageHolder, TargetPlatform::AtariST) // STX Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore) Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric) + Format("tap", result.tapes, Tape::ZXSpectrumTAP, TargetPlatform::ZXSpectrum) // TAP (ZX Spectrum) Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX) // TSX - Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081) // TZX + Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081 | TargetPlatform::ZXSpectrum) // TZX Format("uef", result.tapes, Tape::UEF, TargetPlatform::Acorn) // UEF (tape) Format("woz", result.disks, Disk::DiskImageHolder, TargetPlatform::DiskII) // WOZ @@ -204,27 +207,28 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) { // Hand off to platform-specific determination of whether these things are actually compatible and, // if so, how to load them. - #define Append(x) {\ - auto new_targets = x::GetTargets(media, file_name, potential_platforms);\ - std::move(new_targets.begin(), new_targets.end(), std::back_inserter(targets));\ - } - if(potential_platforms & TargetPlatform::Acorn) Append(Acorn); - if(potential_platforms & TargetPlatform::AmstradCPC) Append(AmstradCPC); - if(potential_platforms & TargetPlatform::AppleII) Append(AppleII); - if(potential_platforms & TargetPlatform::AppleIIgs) Append(AppleIIgs); - if(potential_platforms & TargetPlatform::Atari2600) Append(Atari2600); - if(potential_platforms & TargetPlatform::AtariST) Append(AtariST); - if(potential_platforms & TargetPlatform::ColecoVision) Append(Coleco); - if(potential_platforms & TargetPlatform::Commodore) Append(Commodore); - if(potential_platforms & TargetPlatform::DiskII) Append(DiskII); - if(potential_platforms & TargetPlatform::Macintosh) Append(Macintosh); - if(potential_platforms & TargetPlatform::MSX) Append(MSX); - if(potential_platforms & TargetPlatform::Oric) Append(Oric); - if(potential_platforms & TargetPlatform::Sega) Append(Sega); - if(potential_platforms & TargetPlatform::ZX8081) Append(ZX8081); - #undef Append +#define Append(x) if(potential_platforms & TargetPlatform::x) {\ + auto new_targets = x::GetTargets(media, file_name, potential_platforms);\ + std::move(new_targets.begin(), new_targets.end(), std::back_inserter(targets));\ +} + Append(Acorn); + Append(AmstradCPC); + Append(AppleII); + Append(AppleIIgs); + Append(Atari2600); + Append(AtariST); + Append(Coleco); + Append(Commodore); + Append(DiskII); + Append(Macintosh); + Append(MSX); + Append(Oric); + Append(Sega); + Append(ZX8081); + Append(ZXSpectrum); +#undef Append - // Reset any tapes to their initial position + // Reset any tapes to their initial position. for(const auto &target : targets) { for(auto &tape : target->media.tapes) { tape->reset(); diff --git a/Analyser/Static/ZXSpectrum/StaticAnalyser.cpp b/Analyser/Static/ZXSpectrum/StaticAnalyser.cpp new file mode 100644 index 000000000..0c21161e8 --- /dev/null +++ b/Analyser/Static/ZXSpectrum/StaticAnalyser.cpp @@ -0,0 +1,95 @@ +// +// StaticAnalyser.cpp +// Clock Signal +// +// Created by Thomas Harte on 17/03/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#include "StaticAnalyser.hpp" + +#include "../../../Storage/Disk/Encodings/MFM/Parser.hpp" +#include "../../../Storage/Tape/Parsers/Spectrum.hpp" + +#include "Target.hpp" + +namespace { + +bool IsSpectrumTape(const std::shared_ptr &tape) { + using Parser = Storage::Tape::ZXSpectrum::Parser; + Parser parser(Parser::MachineType::ZXSpectrum); + + while(true) { + const auto block = parser.find_block(tape); + if(!block) break; + + // Check for a Spectrum header block. + if(block->type == 0x00) { + return true; + } + } + + return false; +} + +bool IsSpectrumDisk(const std::shared_ptr &disk) { + Storage::Encodings::MFM::Parser parser(true, disk); + + // Get logical sector 1; the Spectrum appears to support various physical + // sectors as sector 1. + Storage::Encodings::MFM::Sector *boot_sector = nullptr; + uint8_t sector_mask = 0; + while(!boot_sector) { + boot_sector = parser.get_sector(0, 0, sector_mask + 1); + sector_mask += 0x40; + if(!sector_mask) break; + } + if(!boot_sector) return false; + + // Test that the contents of the boot sector sum to 3, modulo 256. + uint8_t byte_sum = 0; + for(auto byte: boot_sector->samples[0]) { + byte_sum += byte; + } + return byte_sum == 3; +} + +} + +Analyser::Static::TargetList Analyser::Static::ZXSpectrum::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) { + TargetList destination; + auto target = std::make_unique(); + target->confidence = 0.5; + + if(!media.tapes.empty()) { + bool has_spectrum_tape = false; + for(auto &tape: media.tapes) { + has_spectrum_tape |= IsSpectrumTape(tape); + } + + if(has_spectrum_tape) { + target->media.tapes = media.tapes; + } + } + + if(!media.disks.empty()) { + bool has_spectrum_disk = false; + + for(auto &disk: media.disks) { + has_spectrum_disk |= IsSpectrumDisk(disk); + } + + if(has_spectrum_disk) { + target->media.disks = media.disks; + target->model = Target::Model::Plus3; + } + } + + // If any media survived, add the target. + if(!target->media.empty()) { + target->should_hold_enter = true; // To force entry into the 'loader' and thereby load the media. + destination.push_back(std::move(target)); + } + + return destination; +} diff --git a/Analyser/Static/ZXSpectrum/StaticAnalyser.hpp b/Analyser/Static/ZXSpectrum/StaticAnalyser.hpp new file mode 100644 index 000000000..756c28ad0 --- /dev/null +++ b/Analyser/Static/ZXSpectrum/StaticAnalyser.hpp @@ -0,0 +1,26 @@ +// +// StaticAnalyser.hpp +// Clock Signal +// +// Created by Thomas Harte on 17/03/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#ifndef Analyser_Static_ZXSpectrum_StaticAnalyser_hpp +#define Analyser_Static_ZXSpectrum_StaticAnalyser_hpp + +#include "../StaticAnalyser.hpp" +#include "../../../Storage/TargetPlatforms.hpp" +#include + +namespace Analyser { +namespace Static { +namespace ZXSpectrum { + +TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); + +} +} +} + +#endif /* StaticAnalyser_hpp */ diff --git a/Analyser/Static/ZXSpectrum/Target.hpp b/Analyser/Static/ZXSpectrum/Target.hpp new file mode 100644 index 000000000..e2711ec55 --- /dev/null +++ b/Analyser/Static/ZXSpectrum/Target.hpp @@ -0,0 +1,41 @@ +// +// Target.hpp +// Clock Signal +// +// Created by Thomas Harte on 18/03/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#ifndef Analyser_Static_ZXSpectrum_Target_h +#define Analyser_Static_ZXSpectrum_Target_h + +#include "../../../Reflection/Enum.hpp" +#include "../../../Reflection/Struct.hpp" +#include "../StaticAnalyser.hpp" + +namespace Analyser { +namespace Static { +namespace ZXSpectrum { + +struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl { + ReflectableEnum(Model, + Plus2a, + Plus3, + ); + + Model model = Model::Plus2a; + bool should_hold_enter = false; + + Target(): Analyser::Static::Target(Machine::ZXSpectrum) { + if(needs_declare()) { + DeclareField(model); + AnnounceEnum(Model); + } + } +}; + +} +} +} + +#endif /* Target_h */ diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index 34a8302fd..4f7c226f0 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -164,6 +164,31 @@ template class AY38910: public ::Outputs::Speaker::SampleSource uint8_t c_left_ = 255, c_right_ = 255; }; +/*! + Provides helper code, to provide something closer to the interface exposed by many + AY-deploying machines of the era. +*/ +struct Utility { + template static void select_register(AY &ay, uint8_t reg) { + ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | GI::AY38910::BC1)); + ay.set_data_input(reg); + ay.set_control_lines(GI::AY38910::ControlLines(0)); + } + + template static void write_data(AY &ay, uint8_t reg) { + ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2)); + ay.set_data_input(reg); + ay.set_control_lines(GI::AY38910::ControlLines(0)); + } + + template static uint8_t read_data(AY &ay) { + ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1)); + const uint8_t result = ay.get_data_output(); + ay.set_control_lines(GI::AY38910::ControlLines(0)); + return result; + } + +}; } } diff --git a/InstructionSets/Sizes.hpp b/InstructionSets/Sizes.hpp index 62f95845a..9d3ed9522 100644 --- a/InstructionSets/Sizes.hpp +++ b/InstructionSets/Sizes.hpp @@ -12,7 +12,7 @@ #include /*! - Maps to the smallest of the following integers that can contain max_value: + Maps to the smallest integral type that can contain max_value, from the following options: * uint8_t; * uint16_t; diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index a5aac6294..316a5d3f7 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -9,12 +9,12 @@ #include "AmstradCPC.hpp" #include "Keyboard.hpp" +#include "FDC.hpp" #include "../../Processors/Z80/Z80.hpp" #include "../../Components/6845/CRTC6845.hpp" #include "../../Components/8255/i8255.hpp" -#include "../../Components/8272/i8272.hpp" #include "../../Components/AY38910/AY38910.hpp" #include "../Utility/MemoryFuzzer.hpp" @@ -676,37 +676,6 @@ class KeyboardState: public GI::AY38910::PortHandler { }; }; -/*! - Wraps the 8272 so as to provide proper clocking and RPM counts, and just directly - exposes motor control, applying the same value to all drives. -*/ -class FDC: public Intel::i8272::i8272 { - private: - Intel::i8272::BusHandler bus_handler_; - - public: - FDC() : i8272(bus_handler_, Cycles(8000000)) { - emplace_drive(8000000, 300, 1); - set_drive(1); - } - - void set_motor_on(bool on) { - get_drive().set_motor_on(on); - } - - void select_drive(int) { - // TODO: support more than one drive. (and in set_disk) - } - - void set_disk(std::shared_ptr disk, int) { - get_drive().set_disk(disk); - } - - void set_activity_observer(Activity::Observer *observer) { - get_drive().set_activity_observer(observer, "Drive 1", true); - } -}; - /*! Provides the mechanism of receipt for input and output of the 8255's various ports. */ @@ -1249,7 +1218,7 @@ template class ConcreteMachine: i8255PortHandler i8255_port_handler_; Intel::i8255::i8255 i8255_; - FDC fdc_; + Amstrad::FDC fdc_; HalfCycles time_since_fdc_update_; void flush_fdc() { if constexpr (has_fdc) { diff --git a/Machines/AmstradCPC/FDC.hpp b/Machines/AmstradCPC/FDC.hpp new file mode 100644 index 000000000..46042e0f5 --- /dev/null +++ b/Machines/AmstradCPC/FDC.hpp @@ -0,0 +1,51 @@ +// +// FDC.hpp +// Clock Signal +// +// Created by Thomas Harte on 22/03/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#ifndef FDC_h +#define FDC_h + +#include "../../Components/8272/i8272.hpp" + +namespace Amstrad { + +/*! + Wraps the 8272 so as to provide proper clocking and RPM counts, and just directly + exposes motor control, applying the same value to all drives. +*/ +class FDC: public Intel::i8272::i8272 { + private: + Intel::i8272::BusHandler bus_handler_; + + public: + FDC(Cycles clock_rate = Cycles(8000000)) : + i8272(bus_handler_, clock_rate) + { + emplace_drive(clock_rate.as(), 300, 1); + set_drive(1); + } + + void set_motor_on(bool on) { + get_drive().set_motor_on(on); + } + + void select_drive(int) { + // TODO: support more than one drive. (and in set_disk) + } + + void set_disk(std::shared_ptr disk, int) { + get_drive().set_disk(disk); + } + + void set_activity_observer(Activity::Observer *observer) { + get_drive().set_activity_observer(observer, "Drive 1", true); + } +}; + +} + +#endif /* FDC_h */ diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp index 30695d0e9..2adc8f38b 100644 --- a/Machines/MSX/MSX.cpp +++ b/Machines/MSX/MSX.cpp @@ -657,7 +657,6 @@ class ConcreteMachine: void set_options(const std::unique_ptr &str) final { const auto options = dynamic_cast(str.get()); - set_video_signal_configurable(options->output); allow_fast_tape_ = options->quickload; set_use_fast_tape(); diff --git a/Machines/ZX8081/Keyboard.cpp b/Machines/Sinclair/Keyboard/Keyboard.cpp similarity index 75% rename from Machines/ZX8081/Keyboard.cpp rename to Machines/Sinclair/Keyboard/Keyboard.cpp index d3e25f381..3677adaca 100644 --- a/Machines/ZX8081/Keyboard.cpp +++ b/Machines/Sinclair/Keyboard/Keyboard.cpp @@ -8,10 +8,14 @@ #include "Keyboard.hpp" -using namespace ZX8081; +#include + +using namespace Sinclair::ZX::Keyboard; + +KeyboardMapper::KeyboardMapper(Machine machine) : machine_(machine) {} uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const { -#define BIND(source, dest) case Inputs::Keyboard::Key::source: return ZX8081::dest +#define BIND(source, dest) case Inputs::Keyboard::Key::source: return dest switch(key) { default: break; @@ -25,10 +29,28 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const { BIND(B, KeyB); BIND(N, KeyN); BIND(M, KeyM); BIND(LeftShift, KeyShift); BIND(RightShift, KeyShift); - BIND(FullStop, KeyDot); BIND(Enter, KeyEnter); BIND(Space, KeySpace); + // Full stop has a key on the ZX80 and ZX81; it doesn't have a dedicated key on the Spectrum. + case Inputs::Keyboard::Key::FullStop: + if(machine_ == Machine::ZXSpectrum) { + return KeySpectrumDot; + } else { + return KeyDot; + } + break; + + // Map controls and options to symbol shift, if this is a ZX Spectrum. + case Inputs::Keyboard::Key::LeftOption: + case Inputs::Keyboard::Key::RightOption: + case Inputs::Keyboard::Key::LeftControl: + case Inputs::Keyboard::Key::RightControl: + if(machine_ == Machine::ZXSpectrum) { + return KeySymbolShift; + } + break; + // Virtual keys follow. BIND(Backspace, KeyDelete); BIND(Escape, KeyBreak); @@ -37,12 +59,13 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const { BIND(Left, KeyLeft); BIND(Right, KeyRight); BIND(BackTick, KeyEdit); BIND(F1, KeyEdit); + BIND(Comma, KeyComma); } #undef BIND return MachineTypes::MappedKeyboardMachine::KeyNotMapped; } -CharacterMapper::CharacterMapper(bool is_zx81) : is_zx81_(is_zx81) {} +CharacterMapper::CharacterMapper(Machine machine) : machine_(machine) {} const uint16_t *CharacterMapper::sequence_for_character(char character) const { #define KEYS(...) {__VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence} @@ -183,12 +206,80 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const { #undef SHIFT #undef X - if(is_zx81_) + switch(machine_) { + case Machine::ZX81: + case Machine::ZXSpectrum: // TODO: some differences exist for the Spectrum. return table_lookup_sequence_for_character(zx81_key_sequences, sizeof(zx81_key_sequences), character); - else + + case Machine::ZX80: return table_lookup_sequence_for_character(zx80_key_sequences, sizeof(zx80_key_sequences), character); + } } bool CharacterMapper::needs_pause_after_key(uint16_t key) const { return key != KeyShift; } + +Keyboard::Keyboard(Machine machine) : machine_(machine) { + clear_all_keys(); +} + +void Keyboard::set_key_state(uint16_t key, bool is_pressed) { + const auto line = key >> 8; + + // Check for special cases. + if(line > 7) { + switch(key) { +#define ShiftedKey(source, base, shift) \ + case source: \ + set_key_state(shift, is_pressed); \ + set_key_state(base, is_pressed); \ + break; + + ShiftedKey(KeyDelete, Key0, KeyShift); + ShiftedKey(KeyBreak, KeySpace, KeyShift); + ShiftedKey(KeyUp, Key7, KeyShift); + ShiftedKey(KeyDown, Key6, KeyShift); + ShiftedKey(KeyLeft, Key5, KeyShift); + ShiftedKey(KeyRight, Key8, KeyShift); + ShiftedKey(KeyEdit, (machine_ == Machine::ZX80) ? KeyEnter : Key1, KeyShift); + + ShiftedKey(KeySpectrumDot, KeyM, KeySymbolShift); + + case KeyComma: + if(machine_ == Machine::ZXSpectrum) { + // Spectrum: comma = symbol shift + n. + set_key_state(KeySymbolShift, is_pressed); + set_key_state(KeyN, is_pressed); + } else { + // ZX80/81: comma = shift + dot. + set_key_state(KeyShift, is_pressed); + set_key_state(KeyDot, is_pressed); + } + break; + +#undef ShiftedKey + } + } else { + if(is_pressed) + key_states_[line] &= uint8_t(~key); + else + key_states_[line] |= uint8_t(key); + } +} + +void Keyboard::clear_all_keys() { + memset(key_states_, 0xff, 8); +} + +uint8_t Keyboard::read(uint16_t address) { + uint8_t value = 0xff; + + uint16_t mask = 0x100; + for(int c = 0; c < 8; c++) { + if(!(address & mask)) value &= key_states_[c]; + mask <<= 1; + } + + return value; +} diff --git a/Machines/ZX8081/Keyboard.hpp b/Machines/Sinclair/Keyboard/Keyboard.hpp similarity index 51% rename from Machines/ZX8081/Keyboard.hpp rename to Machines/Sinclair/Keyboard/Keyboard.hpp index 56cb848ad..25fac9699 100644 --- a/Machines/ZX8081/Keyboard.hpp +++ b/Machines/Sinclair/Keyboard/Keyboard.hpp @@ -9,10 +9,16 @@ #ifndef Machines_ZX8081_Keyboard_hpp #define Machines_ZX8081_Keyboard_hpp -#include "../KeyboardMachine.hpp" -#include "../Utility/Typer.hpp" +#include "../../KeyboardMachine.hpp" +#include "../../Utility/Typer.hpp" -namespace ZX8081 { +namespace Sinclair { +namespace ZX { +namespace Keyboard { + +enum class Machine { + ZX80, ZX81, ZXSpectrum +}; enum Key: uint16_t { KeyShift = 0x0000 | 0x01, KeyZ = 0x0000 | 0x02, KeyX = 0x0000 | 0x04, KeyC = 0x0000 | 0x08, KeyV = 0x0000 | 0x10, @@ -22,28 +28,54 @@ enum Key: uint16_t { Key0 = 0x0400 | 0x01, Key9 = 0x0400 | 0x02, Key8 = 0x0400 | 0x04, Key7 = 0x0400 | 0x08, Key6 = 0x0400 | 0x10, KeyP = 0x0500 | 0x01, KeyO = 0x0500 | 0x02, KeyI = 0x0500 | 0x04, KeyU = 0x0500 | 0x08, KeyY = 0x0500 | 0x10, KeyEnter = 0x0600 | 0x01, KeyL = 0x0600 | 0x02, KeyK = 0x0600 | 0x04, KeyJ = 0x0600 | 0x08, KeyH = 0x0600 | 0x10, - KeySpace = 0x0700 | 0x01, KeyDot = 0x0700 | 0x02, KeyM = 0x0700 | 0x04, KeyN = 0x0700 | 0x08, KeyB = 0x0700 | 0x10, + KeySpace = 0x0700 | 0x01, KeyM = 0x0700 | 0x04, KeyN = 0x0700 | 0x08, KeyB = 0x0700 | 0x10, - // Add some virtual keys; these do not exist on a real ZX80 or ZX81. They're just a convenience. + // The ZX80 and ZX81 keyboards have a full stop; the ZX Spectrum replaces that key with symbol shift. + KeyDot = 0x0700 | 0x02, KeySymbolShift = KeyDot, + + // Add some virtual keys; these do not exist on a real ZX80, ZX81 or early Spectrum, those all were added to the 128kb Spectrums. + // Either way, they're a convenience. KeyDelete = 0x0801, - KeyBreak, KeyLeft, KeyRight, KeyUp, KeyDown, KeyEdit + KeyBreak, KeyLeft, KeyRight, KeyUp, KeyDown, KeyEdit, KeySpectrumDot, KeyComma, }; -struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper { - uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const override; +class Keyboard { + public: + Keyboard(Machine machine); + + void set_key_state(uint16_t key, bool is_pressed); + void clear_all_keys(); + + uint8_t read(uint16_t address); + + private: + uint8_t key_states_[8]; + const Machine machine_; +}; + +class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper { + public: + KeyboardMapper(Machine machine); + + uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const override; + + private: + const Machine machine_; }; class CharacterMapper: public ::Utility::CharacterMapper { public: - CharacterMapper(bool is_zx81); + CharacterMapper(Machine machine); const uint16_t *sequence_for_character(char character) const override; bool needs_pause_after_key(uint16_t key) const override; private: - bool is_zx81_; + const Machine machine_; }; -}; +} +} +} #endif /* KeyboardMapper_hpp */ diff --git a/Machines/ZX8081/Video.cpp b/Machines/Sinclair/ZX8081/Video.cpp similarity index 98% rename from Machines/ZX8081/Video.cpp rename to Machines/Sinclair/ZX8081/Video.cpp index 3ccd75266..da1e72109 100644 --- a/Machines/ZX8081/Video.cpp +++ b/Machines/Sinclair/ZX8081/Video.cpp @@ -10,7 +10,7 @@ #include -using namespace ZX8081; +using namespace Sinclair::ZX8081; namespace { diff --git a/Machines/ZX8081/Video.hpp b/Machines/Sinclair/ZX8081/Video.hpp similarity index 92% rename from Machines/ZX8081/Video.hpp rename to Machines/Sinclair/ZX8081/Video.hpp index fff641a32..555c75603 100644 --- a/Machines/ZX8081/Video.hpp +++ b/Machines/Sinclair/ZX8081/Video.hpp @@ -9,9 +9,10 @@ #ifndef Machines_ZX8081_Video_hpp #define Machines_ZX8081_Video_hpp -#include "../../Outputs/CRT/CRT.hpp" -#include "../../ClockReceiver/ClockReceiver.hpp" +#include "../../../Outputs/CRT/CRT.hpp" +#include "../../../ClockReceiver/ClockReceiver.hpp" +namespace Sinclair { namespace ZX8081 { /*! @@ -57,6 +58,7 @@ class Video { void flush(bool next_sync); }; +} } #endif /* Video_hpp */ diff --git a/Machines/ZX8081/ZX8081.cpp b/Machines/Sinclair/ZX8081/ZX8081.cpp similarity index 89% rename from Machines/ZX8081/ZX8081.cpp rename to Machines/Sinclair/ZX8081/ZX8081.cpp index 0bf86af7d..c57e39f35 100644 --- a/Machines/ZX8081/ZX8081.cpp +++ b/Machines/Sinclair/ZX8081/ZX8081.cpp @@ -8,23 +8,23 @@ #include "ZX8081.hpp" -#include "../MachineTypes.hpp" +#include "../../MachineTypes.hpp" -#include "../../Components/AY38910/AY38910.hpp" -#include "../../Processors/Z80/Z80.hpp" -#include "../../Storage/Tape/Tape.hpp" -#include "../../Storage/Tape/Parsers/ZX8081.hpp" +#include "../../../Components/AY38910/AY38910.hpp" +#include "../../../Processors/Z80/Z80.hpp" +#include "../../../Storage/Tape/Tape.hpp" +#include "../../../Storage/Tape/Parsers/ZX8081.hpp" -#include "../../ClockReceiver/ForceInline.hpp" +#include "../../../ClockReceiver/ForceInline.hpp" -#include "../Utility/MemoryFuzzer.hpp" -#include "../Utility/Typer.hpp" +#include "../../Utility/MemoryFuzzer.hpp" +#include "../../Utility/Typer.hpp" -#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" +#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" -#include "../../Analyser/Static/ZX8081/Target.hpp" +#include "../../../Analyser/Static/ZX8081/Target.hpp" -#include "Keyboard.hpp" +#include "../Keyboard/Keyboard.hpp" #include "Video.hpp" #include @@ -34,7 +34,7 @@ namespace { // The clock rate is 3.25Mhz. - const unsigned int ZX8081ClockRate = 3250000; + constexpr unsigned int ZX8081ClockRate = 3250000; } // TODO: @@ -42,12 +42,15 @@ namespace { // 7FFFh.W PSG index // 7FFEh.R/W PSG data +namespace Sinclair { namespace ZX8081 { enum ROMType: uint8_t { ZX80 = 0, ZX81 }; +using CharacterMapper = Sinclair::ZX::Keyboard::CharacterMapper; + template class ConcreteMachine: public MachineTypes::TimedMachine, public MachineTypes::ScanProducer, @@ -60,14 +63,15 @@ template class ConcreteMachine: public Machine { public: ConcreteMachine(const Analyser::Static::ZX8081::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : - Utility::TypeRecipient(is_zx81), + Utility::TypeRecipient(keyboard_machine()), z80_(*this), + keyboard_(keyboard_machine()), + keyboard_mapper_(keyboard_machine()), tape_player_(ZX8081ClockRate), ay_(GI::AY38910::Personality::AY38910, audio_queue_), speaker_(ay_) { set_clock_rate(ZX8081ClockRate); speaker_.set_input_rate(float(ZX8081ClockRate) / 2.0f); - clear_all_keys(); const bool use_zx81_rom = target.is_ZX81 || target.ZX80_uses_ZX81_ROM; const auto roms = @@ -182,12 +186,7 @@ template class ConcreteMachine: if(!(address&1)) { if(!nmi_is_enabled_) set_vsync(true); - uint16_t mask = 0x100; - for(int c = 0; c < 8; c++) { - if(!(address & mask)) value &= key_states_[c]; - mask <<= 1; - } - + value &= keyboard_.read(address); value &= ~(tape_player_.get_input() ? 0x00 : 0x80); } @@ -338,36 +337,15 @@ template class ConcreteMachine: // MARK: - Keyboard void set_key_state(uint16_t key, bool is_pressed) final { - const auto line = key >> 8; - - // Check for special cases. - if(line > 7) { - switch(key) { -#define ShiftedKey(source, base) \ - case source: \ - set_key_state(KeyShift, is_pressed); \ - set_key_state(base, is_pressed); \ - break; - - ShiftedKey(KeyDelete, Key0); - ShiftedKey(KeyBreak, KeySpace); - ShiftedKey(KeyUp, Key7); - ShiftedKey(KeyDown, Key6); - ShiftedKey(KeyLeft, Key5); - ShiftedKey(KeyRight, Key8); - ShiftedKey(KeyEdit, is_zx81 ? Key1 : KeyEnter); -#undef ShiftedKey - } - } else { - if(is_pressed) - key_states_[line] &= uint8_t(~key); - else - key_states_[line] |= uint8_t(key); - } + keyboard_.set_key_state(key, is_pressed); } void clear_all_keys() final { - memset(key_states_, 0xff, 8); + keyboard_.clear_all_keys(); + } + + static constexpr Sinclair::ZX::Keyboard::Machine keyboard_machine() { + return is_zx81 ? Sinclair::ZX::Keyboard::Machine::ZX81 : Sinclair::ZX::Keyboard::Machine::ZX80; } // MARK: - Tape control @@ -401,6 +379,7 @@ template class ConcreteMachine: } // MARK: - Configuration options. + std::unique_ptr get_options() final { auto options = std::make_unique(Configurable::OptionsType::UserFriendly); // OptionsType is arbitrary, but not optional. options->automatic_tape_motor_control = use_automatic_tape_motor_control_; @@ -447,8 +426,8 @@ template class ConcreteMachine: bool vsync_ = false, hsync_ = false; int line_counter_ = 0; - uint8_t key_states_[8]; - ZX8081::KeyboardMapper keyboard_mapper_; + Sinclair::ZX::Keyboard::Keyboard keyboard_; + Sinclair::ZX::Keyboard::KeyboardMapper keyboard_mapper_; HalfClockReceiver tape_player_; Storage::Tape::ZX8081::Parser parser_; @@ -515,17 +494,18 @@ template class ConcreteMachine: } }; +} } -using namespace ZX8081; +using namespace Sinclair::ZX8081; // See header; constructs and returns an instance of the ZX80 or 81. Machine *Machine::ZX8081(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { - const Analyser::Static::ZX8081::Target *const zx_target = dynamic_cast(target); + const auto zx_target = dynamic_cast(target); // Instantiate the correct type of machine. - if(zx_target->is_ZX81) return new ZX8081::ConcreteMachine(*zx_target, rom_fetcher); - else return new ZX8081::ConcreteMachine(*zx_target, rom_fetcher); + if(zx_target->is_ZX81) return new ConcreteMachine(*zx_target, rom_fetcher); + else return new ConcreteMachine(*zx_target, rom_fetcher); } Machine::~Machine() {} diff --git a/Machines/ZX8081/ZX8081.hpp b/Machines/Sinclair/ZX8081/ZX8081.hpp similarity index 84% rename from Machines/ZX8081/ZX8081.hpp rename to Machines/Sinclair/ZX8081/ZX8081.hpp index 5fff0ffe8..d16960dca 100644 --- a/Machines/ZX8081/ZX8081.hpp +++ b/Machines/Sinclair/ZX8081/ZX8081.hpp @@ -9,20 +9,20 @@ #ifndef ZX8081_hpp #define ZX8081_hpp -#include "../../Configurable/Configurable.hpp" -#include "../../Configurable/StandardOptions.hpp" -#include "../../Analyser/Static/StaticAnalyser.hpp" -#include "../ROMMachine.hpp" +#include "../../../Configurable/Configurable.hpp" +#include "../../../Configurable/StandardOptions.hpp" +#include "../../../Analyser/Static/StaticAnalyser.hpp" +#include "../../ROMMachine.hpp" #include +namespace Sinclair { namespace ZX8081 { /// The ZX80/81 machine. class Machine { public: virtual ~Machine(); - static Machine *ZX8081(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); virtual void set_tape_is_playing(bool is_playing) = 0; @@ -47,6 +47,7 @@ class Machine { }; }; +} } #endif /* ZX8081_hpp */ diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp new file mode 100644 index 000000000..2ac8a36b6 --- /dev/null +++ b/Machines/Sinclair/ZXSpectrum/Video.hpp @@ -0,0 +1,384 @@ +// +// Video.hpp +// Clock Signal +// +// Created by Thomas Harte on 18/03/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#ifndef Video_hpp +#define Video_hpp + +#include "../../../Outputs/CRT/CRT.hpp" +#include "../../../ClockReceiver/ClockReceiver.hpp" + +#include + +namespace Sinclair { +namespace ZXSpectrum { + +enum class VideoTiming { + Plus3 +}; + +/* + Timing notes: + + As of the +2a/+3: + + 311 lines, 228 cycles/line + Delays begin at 14361, follow the pattern 1, 0, 7, 6, 5, 4, 3, 2; run for 129 cycles/line. + Possibly delays only affect actual reads and writes; documentation is unclear. + + Unknowns, to me, presently: + + How long the interrupt line held for. + + So... + + Probably two bytes of video and attribute are fetched in each 8-cycle block, + with 16 such blocks therefore providing the whole visible display, an island + within 28.5 blocks horizontally. + + 14364 is 228*63, so I I guess almost 63 lines run from the start of vertical + blank through to the top of the display, implying 56 lines on to vertical blank. + +*/ + +template class Video { + private: + struct Timings { + // Number of cycles per line. Will be 224 or 228. + int cycles_per_line; + // Number of lines comprising a whole frame. Will be 311 or 312. + int lines_per_frame; + + // Number of cycles after first pixel fetch at which interrupt is first signalled. + int interrupt_time; + + // Number of cycles before first pixel fetch that contention starts to be applied. + int contention_leadin; + // Period in a line for which contention is applied. + int contention_duration; + + // Contention to apply, in half-cycles, as a function of number of half cycles since + // contention began. + int delays[16]; + }; + + static constexpr Timings get_timings() { + // Amstrad gate array timings, classic statement: + // + // Contention begins 14361 cycles "after interrupt" and follows the pattern [1, 0, 7, 6 5 4, 3, 2]. + // The first four bytes of video are fetched at 14365–14368 cycles, in the order [pixels, attribute, pixels, attribute]. + // + // For my purposes: + // + // Video fetching always begins at 0. Since there are 311*228 = 70908 cycles per frame, and the interrupt + // should "occur" (I assume: begin) 14365 before that, it should actually begin at 70908 - 14365 = 56543. + // + // Contention begins four cycles before the first video fetch, so it begins at 70904. I don't currently + // know whether the four cycles is true across all models, so it's given here as convention_leadin. + // + // ... except that empirically that all seems to be two cycles off. So maybe I misunderstand what the + // contention patterns are supposed to indicate relative to MREQ? It's frustrating that all documentation + // I can find is vaguely in terms of contention patterns, and what they mean isn't well-defined in terms + // of regular Z80 signalling. + constexpr Timings result = { + .cycles_per_line = 228 * 2, + .lines_per_frame = 311, + + .interrupt_time = 56545 * 2, + + .contention_leadin = 2 * 2, // TODO: is this 2? Or 4? Or... ? + .contention_duration = 129 * 2, + + .delays = { + 2, 1, + 0, 0, + 14, 13, + 12, 11, + 10, 9, + 8, 7, + 6, 5, + 4, 3, + } + }; + return result; + } + + // TODO: how long is the interrupt line held for? + static constexpr int interrupt_duration = 48; + + public: + void run_for(HalfCycles duration) { + constexpr auto timings = get_timings(); + + constexpr int sync_line = (timings.interrupt_time / timings.cycles_per_line) + 1; + + constexpr int sync_position = 166 * 2; + constexpr int sync_length = 17 * 2; + constexpr int burst_position = sync_position + 40; + constexpr int burst_length = 17; + + int cycles_remaining = duration.as(); + while(cycles_remaining) { + int line = time_into_frame_ / timings.cycles_per_line; + int offset = time_into_frame_ % timings.cycles_per_line; + const int cycles_this_line = std::min(cycles_remaining, timings.cycles_per_line - offset); + const int end_offset = offset + cycles_this_line; + + if(!offset) { + is_alternate_line_ ^= true; + + if(!line) { + flash_counter_ = (flash_counter_ + 1) & 31; + flash_mask_ = uint8_t(flash_counter_ >> 4); + } + } + + if(line >= sync_line && line < sync_line + 3) { + // Output sync line. + crt_.output_sync(cycles_this_line); + } else { + if(line >= 192) { + // Output plain border line. + if(offset < sync_position) { + const int border_duration = std::min(sync_position, end_offset) - offset; + output_border(border_duration); + offset += border_duration; + } + } else { + // Output pixel line. + if(offset < 256) { + const int pixel_duration = std::min(256, end_offset) - offset; + + if(!offset) { + pixel_target_ = crt_.begin_data(256); + attribute_address_ = ((line >> 3) << 5) + 6144; + pixel_address_ = ((line & 0x07) << 8) | ((line & 0x38) << 2) | ((line & 0xc0) << 5); + } + + if(pixel_target_) { + const int start_column = offset >> 4; + const int end_column = (offset + pixel_duration) >> 4; + for(int column = start_column; column < end_column; column++) { + last_fetches_[0] = memory_[pixel_address_]; + last_fetches_[1] = memory_[attribute_address_]; + last_fetches_[2] = memory_[pixel_address_+1]; + last_fetches_[3] = memory_[attribute_address_+1]; + pixel_address_ += 2; + attribute_address_ += 2; + + constexpr uint8_t masks[] = {0, 0xff}; + +#define Output(n) \ + { \ + const uint8_t pixels = \ + uint8_t(last_fetches_[n] ^ masks[flash_mask_ & (last_fetches_[n+1] >> 7)]); \ + \ + const uint8_t colours[2] = { \ + palette[(last_fetches_[n+1] & 0x78) >> 3], \ + palette[((last_fetches_[n+1] & 0x40) >> 3) | (last_fetches_[n+1] & 0x07)], \ + }; \ + \ + pixel_target_[0] = colours[(pixels >> 7) & 1]; \ + pixel_target_[1] = colours[(pixels >> 6) & 1]; \ + pixel_target_[2] = colours[(pixels >> 5) & 1]; \ + pixel_target_[3] = colours[(pixels >> 4) & 1]; \ + pixel_target_[4] = colours[(pixels >> 3) & 1]; \ + pixel_target_[5] = colours[(pixels >> 2) & 1]; \ + pixel_target_[6] = colours[(pixels >> 1) & 1]; \ + pixel_target_[7] = colours[(pixels >> 0) & 1]; \ + pixel_target_ += 8; \ + } + + Output(0); + Output(2); + +#undef Output + } + } + + offset += pixel_duration; + if(offset == 256) { + crt_.output_data(256); + pixel_target_ = nullptr; + } + } + + if(offset >= 256 && offset < sync_position && end_offset > offset) { + const int border_duration = std::min(sync_position, end_offset) - offset; + output_border(border_duration); + offset += border_duration; + } + } + + // Output the common tail to border and pixel lines: sync, blank, colour burst, border. + + if(offset >= sync_position && offset < sync_position + sync_length && end_offset > offset) { + const int sync_duration = std::min(sync_position + sync_length, end_offset) - offset; + crt_.output_sync(sync_duration); + offset += sync_duration; + } + + if(offset >= sync_position + sync_length && offset < burst_position && end_offset > offset) { + const int blank_duration = std::min(burst_position, end_offset) - offset; + crt_.output_blank(blank_duration); + offset += blank_duration; + } + + if(offset >= burst_position && offset < burst_position+burst_length && end_offset > offset) { + const int burst_duration = std::min(burst_position + burst_length, end_offset) - offset; + crt_.output_colour_burst(burst_duration, 116, is_alternate_line_); + offset += burst_duration; + // The colour burst phase above is an empirical guess. I need to research further. + } + + if(offset >= burst_position+burst_length && end_offset > offset) { + const int border_duration = end_offset - offset; + output_border(border_duration); + } + } + + cycles_remaining -= cycles_this_line; + time_into_frame_ = (time_into_frame_ + cycles_this_line) % (timings.cycles_per_line * timings.lines_per_frame); + } + } + + private: + void output_border(int duration) { + uint8_t *const colour_pointer = crt_.begin_data(1); + if(colour_pointer) *colour_pointer = border_colour_; + crt_.output_level(duration); + } + + public: + Video() : + crt_(227 * 2, 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2) + { + // Show only the centre 80% of the TV frame. + crt_.set_display_type(Outputs::Display::DisplayType::RGB); + crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f)); + + } + + void set_video_source(const uint8_t *source) { + memory_ = source; + } + + /*! + @returns The amount of time until the next change in the interrupt line, that being the only internally-observeable output. + */ + HalfCycles get_next_sequence_point() { + constexpr auto timings = get_timings(); + + // Is the frame still ahead of this interrupt? + if(time_into_frame_ < timings.interrupt_time) { + return HalfCycles(timings.interrupt_time - time_into_frame_); + } + + // If not, is it within this interrupt? + if(time_into_frame_ < timings.interrupt_time + interrupt_duration) { + return HalfCycles(timings.interrupt_time + interrupt_duration - time_into_frame_); + } + + // If not, it'll be in the next batch. + return timings.interrupt_time + timings.cycles_per_line * timings.lines_per_frame - time_into_frame_; + } + + /*! + @returns The current state of the interrupt output. + */ + bool get_interrupt_line() const { + constexpr auto timings = get_timings(); + return time_into_frame_ >= timings.interrupt_time && time_into_frame_ < timings.interrupt_time + interrupt_duration; + } + + /*! + @returns How many cycles the [ULA/gate array] would delay the CPU for if it were to recognise that contention + needs to be applied in @c offset half-cycles from now. + */ + int access_delay(HalfCycles offset) const { + constexpr auto timings = get_timings(); + const int delay_time = (time_into_frame_ + offset.as() + timings.contention_leadin) % (timings.cycles_per_line * timings.lines_per_frame); + + // Check for a time within the no-contention window. + if(delay_time >= (191*timings.cycles_per_line + timings.contention_duration)) { + return 0; + } + + const int time_into_line = delay_time % timings.cycles_per_line; + if(time_into_line >= timings.contention_duration) return 0; + + return timings.delays[time_into_line & 15]; + } + + /*! + @returns Whatever the ULA or gate array has fetched this cycle, or 0xff if it has fetched nothing. + */ + uint8_t get_current_fetch() const { + constexpr auto timings = get_timings(); + const int line = time_into_frame_ / timings.cycles_per_line; + if(line >= 192) return 0xff; + + const int time_into_line = time_into_frame_ % timings.cycles_per_line; + if(time_into_line >= 256 || (time_into_line&4)) { + return 0xff; + } + + return last_fetches_[time_into_line & 3]; + } + + /*! + Sets the current border colour. + */ + void set_border_colour(uint8_t colour) { + border_colour_ = palette[colour]; + } + + /// Sets the scan target. + void set_scan_target(Outputs::Display::ScanTarget *scan_target) { + crt_.set_scan_target(scan_target); + } + + /// Gets the current scan status. + Outputs::Display::ScanStatus get_scaled_scan_status() const { + return crt_.get_scaled_scan_status(); + } + + /*! Sets the type of display the CRT will request. */ + void set_display_type(Outputs::Display::DisplayType type) { + crt_.set_display_type(type); + } + + private: + int time_into_frame_ = 0; + Outputs::CRT::CRT crt_; + const uint8_t *memory_ = nullptr; + uint8_t border_colour_ = 0; + + uint8_t *pixel_target_ = nullptr; + int attribute_address_ = 0; + int pixel_address_ = 0; + + uint8_t flash_mask_ = 0; + int flash_counter_ = 0; + bool is_alternate_line_ = false; + + uint8_t last_fetches_[4] = {0xff, 0xff, 0xff, 0xff}; + +#define RGB(r, g, b) (r << 4) | (g << 2) | b + static constexpr uint8_t palette[] = { + RGB(0, 0, 0), RGB(0, 0, 2), RGB(2, 0, 0), RGB(2, 0, 2), + RGB(0, 2, 0), RGB(0, 2, 2), RGB(2, 2, 0), RGB(2, 2, 2), + RGB(0, 0, 0), RGB(0, 0, 3), RGB(3, 0, 0), RGB(3, 0, 3), + RGB(0, 3, 0), RGB(0, 3, 3), RGB(3, 3, 0), RGB(3, 3, 3), + }; +#undef RGB +}; + +} +} + +#endif /* Video_hpp */ diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp new file mode 100644 index 000000000..4bb9ec581 --- /dev/null +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -0,0 +1,639 @@ +// +// ZXSpectrum.cpp +// Clock Signal +// +// Created by Thomas Harte on 17/03/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#include "ZXSpectrum.hpp" + +#include "Video.hpp" + +#define LOG_PREFIX "[Spectrum] " + +#include "../../MachineTypes.hpp" + +#include "../../../Processors/Z80/Z80.hpp" + +#include "../../../Components/AudioToggle/AudioToggle.hpp" +#include "../../../Components/AY38910/AY38910.hpp" + +// TODO: possibly there's a better factoring than this, but for now +// just grab the CPC's version of an FDC. +#include "../../AmstradCPC/FDC.hpp" + +#include "../../../Outputs/Log.hpp" +#include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp" +#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" +#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" + +#include "../../../Storage/Tape/Tape.hpp" +#include "../../../Storage/Tape/Parsers/Spectrum.hpp" + +#include "../../../Analyser/Static/ZXSpectrum/Target.hpp" + +#include "../../Utility/MemoryFuzzer.hpp" + +#include "../../../ClockReceiver/JustInTime.hpp" + +#include "../Keyboard/Keyboard.hpp" + +#include + +namespace Sinclair { +namespace ZXSpectrum { + +using Model = Analyser::Static::ZXSpectrum::Target::Model; +template class ConcreteMachine: + public Configurable::Device, + public Machine, + public MachineTypes::AudioProducer, + public MachineTypes::MappedKeyboardMachine, + public MachineTypes::MediaTarget, + public MachineTypes::ScanProducer, + public MachineTypes::TimedMachine, + public CPU::Z80::BusHandler { + public: + ConcreteMachine(const Analyser::Static::ZXSpectrum::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : + z80_(*this), + ay_(GI::AY38910::Personality::AY38910, audio_queue_), + audio_toggle_(audio_queue_), + mixer_(ay_, audio_toggle_), + speaker_(mixer_), + keyboard_(Sinclair::ZX::Keyboard::Machine::ZXSpectrum), + keyboard_mapper_(Sinclair::ZX::Keyboard::Machine::ZXSpectrum), + tape_player_(clock_rate() * 2), + fdc_(clock_rate() * 2) + { + set_clock_rate(clock_rate()); + speaker_.set_input_rate(float(clock_rate()) / 2.0f); + + // With only the +2a and +3 currently supported, the +3 ROM is always + // the one required. + const auto roms = + rom_fetcher({ {"ZXSpectrum", "the +2a/+3 ROM", "plus3.rom", 64 * 1024, 0x96e3c17a} }); + if(!roms[0]) throw ROMMachine::Error::MissingROMs; + memcpy(rom_.data(), roms[0]->data(), std::min(rom_.size(), roms[0]->size())); + + // Set up initial memory map. + update_memory_map(); + set_video_address(); + Memory::Fuzz(ram_); + + // Insert media. + insert_media(target.media); + + // Possibly depress the enter key. + if(target.should_hold_enter) { + // Hold it for five seconds, more or less. + duration_to_press_enter_ = Cycles(5 * clock_rate()); + keyboard_.set_key_state(ZX::Keyboard::KeyEnter, true); + } + } + + ~ConcreteMachine() { + audio_queue_.flush(); + } + + static constexpr unsigned int clock_rate() { +// constexpr unsigned int ClockRate = 3'500'000; + constexpr unsigned int Plus3ClockRate = 3'546'875; // See notes below; this is a guess. + + // Notes on timing for the +2a and +3: + // + // Standard PAL produces 283.7516 colour cycles per line, each line being 64µs. + // The oft-quoted 3.5469 Mhz would seem to imply 227.0016 clock cycles per line. + // Since those Spectrums actually produce 228 cycles per line, but software like + // Chromatrons seems to assume a fixed phase relationship, I guess that the real + // clock speed is whatever gives: + // + // 228 / [cycles per line] * 283.7516 = [an integer]. + // + // i.e. 228 * 283.7516 = [an integer] * [cycles per line], such that cycles per line ~= 227 + // ... which would imply that 'an integer' is probably 285, i.e. + // + // 228 / [cycles per line] * 283.7516 = 285 + // => 227.00128 = [cycles per line] + // => clock rate = 3.546895 Mhz? + // + // That is... unless I'm mistaken about the PAL colour subcarrier and it's actually 283.75, + // which would give exactly 227 cycles/line and therefore 3.546875 Mhz. + // + // A real TV would be likely to accept either, I guess. But it does seem like + // the Spectrum is a PAL machine with a fixed colour phase relationship. For + // this emulator's world, that's a first! + + return Plus3ClockRate; + } + + // MARK: - TimedMachine + + void run_for(const Cycles cycles) override { + z80_.run_for(cycles); + + // Use this very broad timing base for the automatic enter depression. + // It's not worth polluting the main loop. + if(duration_to_press_enter_ > Cycles(0)) { + if(duration_to_press_enter_ < cycles) { + duration_to_press_enter_ = Cycles(0); + keyboard_.set_key_state(ZX::Keyboard::KeyEnter, false); + } else { + duration_to_press_enter_ -= cycles; + } + } + } + + void flush() { + video_.flush(); + update_audio(); + audio_queue_.perform(); + + if constexpr (model == Model::Plus3) { + fdc_.flush(); + } + } + + // MARK: - ScanProducer + + void set_scan_target(Outputs::Display::ScanTarget *scan_target) override { + video_->set_scan_target(scan_target); + } + + Outputs::Display::ScanStatus get_scaled_scan_status() const override { + return video_->get_scaled_scan_status(); + } + + void set_display_type(Outputs::Display::DisplayType display_type) override { + video_->set_display_type(display_type); + } + + // MARK: - BusHandler + + forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) { + using PartialMachineCycle = CPU::Z80::PartialMachineCycle; + + HalfCycles delay(0); + const uint16_t address = cycle.address ? *cycle.address : 0x0000; + switch(cycle.operation) { + default: break; + + case PartialMachineCycle::ReadOpcodeStart: + case PartialMachineCycle::ReadStart: + case PartialMachineCycle::WriteStart: + // Apply contention if necessary. + // + // Assumption here: the trigger for the ULA inserting a delay is the falling edge + // of MREQ, which is always half a cycle into a read or write. + // + // TODO: somehow provide that information in the PartialMachineCycle? + if(is_contended_[address >> 14]) { + delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1)); + } + break; + + case PartialMachineCycle::ReadOpcode: + // Fast loading: ROM version. + // + // The below patches over the 'LD-BYTES' routine from the 48kb ROM. + if(use_fast_tape_hack_ && address == 0x0556 && read_pointers_[0] == &rom_[0xc000]) { + if(perform_rom_ld_bytes()) { + // Stop pressing enter, if neccessry. + if(duration_to_press_enter_ > Cycles(0)) { + duration_to_press_enter_ = Cycles(0); + keyboard_.set_key_state(ZX::Keyboard::KeyEnter, false); + } + + *cycle.value = 0xc9; // i.e. RET. + break; + } + } + + case PartialMachineCycle::Read: + *cycle.value = read_pointers_[address >> 14][address]; + break; + + case PartialMachineCycle::Write: + // Flush video if this access modifies screen contents. + if(is_video_[address >> 14] && (address & 0x3fff) < 6912) { + video_.flush(); + } + write_pointers_[address >> 14][address] = *cycle.value; + break; + + case PartialMachineCycle::Output: + // Test for port FE. + if(!(address&1)) { + update_audio(); + audio_toggle_.set_output(*cycle.value & 0x10); + + video_->set_border_colour(*cycle.value & 7); + + // b0–b2: border colour + // b3: enable tape input (?) + // b4: tape and speaker output + } + + // Test for classic 128kb paging register (i.e. port 7ffd). + if((address & 0xc002) == 0x4000) { + port7ffd_ = *cycle.value; + update_memory_map(); + + // Set the proper video base pointer. + set_video_address(); + + // Potentially lock paging, _after_ the current + // port values have taken effect. + disable_paging_ |= *cycle.value & 0x20; + } + + // Test for +2a/+3 paging (i.e. port 1ffd). + if((address & 0xf002) == 0x1000) { + port1ffd_ = *cycle.value; + update_memory_map(); + update_video_base(); + + if constexpr (model == Model::Plus3) { + fdc_->set_motor_on(*cycle.value & 0x08); + } + } + + if((address & 0xc002) == 0xc000) { + // Select AY register. + update_audio(); + GI::AY38910::Utility::select_register(ay_, *cycle.value); + } + + if((address & 0xc002) == 0x8000) { + // Write to AY register. + update_audio(); + GI::AY38910::Utility::write_data(ay_, *cycle.value); + } + + if constexpr (model == Model::Plus3) { + switch(address) { + default: break; + case 0x3ffd: case 0x2ffd: + fdc_->write((address >> 12) & 1, *cycle.value); + break; + } + } + break; + + case PartialMachineCycle::Input: + *cycle.value = 0xff; + + if(!(address&1)) { + // Port FE: + // + // address b8+: mask of keyboard lines to select + // result: b0–b4: mask of keys pressed + // b6: tape input + + *cycle.value &= keyboard_.read(address); + *cycle.value &= tape_player_.get_input() ? 0xbf : 0xff; + + // If this read is within 200 cycles of the previous, + // count it as an adjacent hit; if 20 of those have + // occurred then start the tape motor. + if(use_automatic_tape_motor_control_) { + if(cycles_since_tape_input_read_ < HalfCycles(400)) { + ++recent_tape_hits_; + + if(recent_tape_hits_ == 20) { + tape_player_.set_motor_control(true); + } + } else { + recent_tape_hits_ = 0; + } + + cycles_since_tape_input_read_ = HalfCycles(0); + } + } + + if((address & 0xc002) == 0xc000) { + // Read from AY register. + update_audio(); + *cycle.value &= GI::AY38910::Utility::read_data(ay_); + } + + if constexpr (model == Model::Plus3) { + switch(address) { + default: break; + case 0x3ffd: case 0x2ffd: + *cycle.value &= fdc_->read((address >> 12) & 1); + break; + } + } + break; + } + + advance(cycle.length + delay); + return delay; + } + + private: + void advance(HalfCycles duration) { + time_since_audio_update_ += duration; + + video_ += duration; + if(video_.did_flush()) { + z80_.set_interrupt_line(video_.last_valid()->get_interrupt_line()); + } + + // TODO: sleeping support here. + tape_player_.run_for(duration.as_integral()); + + // Update automatic tape motor control, if enabled; if it's been + // 3 seconds since software last possibly polled the tape, stop it. + if(use_automatic_tape_motor_control_ && cycles_since_tape_input_read_ < HalfCycles(clock_rate() * 6)) { + cycles_since_tape_input_read_ += duration; + + if(cycles_since_tape_input_read_ >= HalfCycles(clock_rate() * 6)) { + tape_player_.set_motor_control(false); + recent_tape_hits_ = 0; + } + } + + if constexpr (model == Model::Plus3) { + fdc_ += Cycles(duration.as_integral()); + } + } + + public: + + // MARK: - Typer +// HalfCycles get_typer_delay(const std::string &) const final { +// return z80_.get_is_resetting() ? Cycles(7'000'000) : Cycles(0); +// } +// +// HalfCycles get_typer_frequency() const final { +// return Cycles(146'250); +// } + + KeyboardMapper *get_keyboard_mapper() override { + return &keyboard_mapper_; + } + + // MARK: - Keyboard + void set_key_state(uint16_t key, bool is_pressed) override { + keyboard_.set_key_state(key, is_pressed); + } + + void clear_all_keys() override { + keyboard_.clear_all_keys(); + + // Caveat: if holding enter synthetically, continue to do so. + if(duration_to_press_enter_ > Cycles(0)) { + keyboard_.set_key_state(ZX::Keyboard::KeyEnter, true); + } + } + + // MARK: - MediaTarget. + bool insert_media(const Analyser::Static::Media &media) override { + // If there are any tapes supplied, use the first of them. + if(!media.tapes.empty()) { + tape_player_.set_tape(media.tapes.front()); + } + + // Insert up to four disks. + int c = 0; + for(auto &disk : media.disks) { + fdc_->set_disk(disk, c); + c++; + if(c == 4) break; + } + + return !media.tapes.empty() || (!media.disks.empty() && model == Model::Plus3); + } + + // MARK: - Tape control + + void set_use_automatic_tape_motor_control(bool enabled) { + use_automatic_tape_motor_control_ = enabled; + if(!enabled) { + tape_player_.set_motor_control(false); + } + } + + void set_tape_is_playing(bool is_playing) final { + tape_player_.set_motor_control(is_playing); + } + + bool get_tape_is_playing() final { + return tape_player_.get_motor_control(); + } + + // MARK: - Configuration options. + + std::unique_ptr get_options() override { + auto options = std::make_unique(Configurable::OptionsType::UserFriendly); // OptionsType is arbitrary, but not optional. + options->automatic_tape_motor_control = use_automatic_tape_motor_control_; + options->quickload = allow_fast_tape_hack_; + return options; + } + + void set_options(const std::unique_ptr &str) override { + const auto options = dynamic_cast(str.get()); + set_video_signal_configurable(options->output); + set_use_automatic_tape_motor_control(options->automatic_tape_motor_control); + allow_fast_tape_hack_ = options->quickload; + set_use_fast_tape(); + } + + // MARK: - AudioProducer. + + Outputs::Speaker::Speaker *get_speaker() override { + return &speaker_; + } + + private: + CPU::Z80::Processor z80_; + + // MARK: - Memory. + std::array rom_; + std::array ram_; + + std::array scratch_; + const uint8_t *read_pointers_[4]; + uint8_t *write_pointers_[4]; + uint8_t pages_[4]; + bool is_contended_[4]; + bool is_video_[4]; + + uint8_t port1ffd_ = 0; + uint8_t port7ffd_ = 0; + bool disable_paging_ = false; + + void update_memory_map() { + // If paging is permanently disabled, don't react. + if(disable_paging_) { + return; + } + + if(port1ffd_ & 0x01) { + // "Special paging mode", i.e. one of four fixed + // RAM configurations, port 7ffd doesn't matter. + + switch(port1ffd_ & 0x06) { + default: + case 0x00: + set_memory(0, 0); + set_memory(1, 1); + set_memory(2, 2); + set_memory(3, 3); + break; + + case 0x02: + set_memory(0, 4); + set_memory(1, 5); + set_memory(2, 6); + set_memory(3, 7); + break; + + case 0x04: + set_memory(0, 4); + set_memory(1, 5); + set_memory(2, 6); + set_memory(3, 3); + break; + + case 0x06: + set_memory(0, 4); + set_memory(1, 7); + set_memory(2, 6); + set_memory(3, 3); + break; + } + } else { + // Apply standard 128kb-esque mapping (albeit with extra ROM to pick from). + set_memory(0, 0x80 | ((port1ffd_ >> 1) & 2) | ((port7ffd_ >> 4) & 1)); + set_memory(1, 5); + set_memory(2, 2); + set_memory(3, port7ffd_ & 7); + } + } + + void set_memory(int bank, uint8_t source) { + is_contended_[bank] = (source >= 4 && source < 8); + pages_[bank] = source; + + uint8_t *read = (source < 0x80) ? &ram_[source * 16384] : &rom_[(source & 0x7f) * 16384]; + const auto offset = bank*16384; + + read_pointers_[bank] = read - offset; + write_pointers_[bank] = ((source < 0x80) ? read : scratch_.data()) - offset; + } + + void set_video_address() { + video_->set_video_source(&ram_[((port7ffd_ & 0x08) ? 7 : 5) * 16384]); + update_video_base(); + } + + void update_video_base() { + const uint8_t video_page = (port7ffd_ & 0x08) ? 7 : 5; + is_video_[0] = pages_[0] == video_page; + is_video_[1] = pages_[1] == video_page; + is_video_[2] = pages_[2] == video_page; + is_video_[3] = pages_[3] == video_page; + } + + // MARK: - Audio. + Concurrency::DeferringAsyncTaskQueue audio_queue_; + GI::AY38910::AY38910 ay_; + Audio::Toggle audio_toggle_; + Outputs::Speaker::CompoundSource, Audio::Toggle> mixer_; + Outputs::Speaker::LowpassSpeaker, Audio::Toggle>> speaker_; + + HalfCycles time_since_audio_update_; + void update_audio() { + speaker_.run_for(audio_queue_, time_since_audio_update_.divide_cycles(Cycles(2))); + } + + // MARK: - Video. + static constexpr VideoTiming video_timing = VideoTiming::Plus3; + JustInTimeActor> video_; + + // MARK: - Keyboard. + Sinclair::ZX::Keyboard::Keyboard keyboard_; + Sinclair::ZX::Keyboard::KeyboardMapper keyboard_mapper_; + + // MARK: - Tape. + Storage::Tape::BinaryTapePlayer tape_player_; + + bool use_automatic_tape_motor_control_ = true; + HalfCycles cycles_since_tape_input_read_; + int recent_tape_hits_ = 0; + + bool allow_fast_tape_hack_ = false; + bool use_fast_tape_hack_ = false; + void set_use_fast_tape() { + use_fast_tape_hack_ = allow_fast_tape_hack_ && tape_player_.has_tape(); + } + + // Reimplements the 'LD-BYTES' routine, as documented at + // https://skoolkid.github.io/rom/asm/0556.html i.e. + // + // In: + // A: 0x00 or 0xff for block type; + // F: carry set if loading, clear if verifying; + // DE: block length; + // IX: start address. + // + // Out: + // F: carry set for success, clear for error. + bool perform_rom_ld_bytes() { + using Parser = Storage::Tape::ZXSpectrum::Parser; + Parser parser(Parser::MachineType::ZXSpectrum); + + using Register = CPU::Z80::Register; + uint8_t flags = uint8_t(z80_.get_value_of_register(Register::Flags)); + if(!(flags & 1)) return false; + + const uint8_t block_type = uint8_t(z80_.get_value_of_register(Register::A)); + const auto block = parser.find_block(tape_player_.get_tape()); + if(!block || block_type != (*block).type) return false; + + uint16_t length = z80_.get_value_of_register(Register::DE); + uint16_t target = z80_.get_value_of_register(Register::IX); + + while(length--) { + auto next = parser.get_byte(tape_player_.get_tape()); + if(!next) { + flags &= ~1; + break; + } + + write_pointers_[target >> 14][target] = *next; + ++target; + } + + z80_.set_value_of_register(Register::Flags, flags); + return true; + } + + // MARK: - Disc. + JustInTimeActor fdc_; + + // MARK: - Automatic startup. + Cycles duration_to_press_enter_; +}; + + +} +} + +using namespace Sinclair::ZXSpectrum; + +Machine *Machine::ZXSpectrum(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { + const auto zx_target = dynamic_cast(target); + + switch(zx_target->model) { + case Model::Plus2a: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::Plus3: return new ConcreteMachine(*zx_target, rom_fetcher); + } + + return nullptr; +} + +Machine::~Machine() {} diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.hpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.hpp new file mode 100644 index 000000000..693cc3164 --- /dev/null +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.hpp @@ -0,0 +1,54 @@ +// +// ZXSpectrum.hpp +// Clock Signal +// +// Created by Thomas Harte on 17/03/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#ifndef ZXSpectrum_hpp +#define ZXSpectrum_hpp + +#include "../../../Configurable/Configurable.hpp" +#include "../../../Configurable/StandardOptions.hpp" +#include "../../../Analyser/Static/StaticAnalyser.hpp" +#include "../../ROMMachine.hpp" + +#include + +namespace Sinclair { +namespace ZXSpectrum { + +class Machine { + public: + virtual ~Machine(); + static Machine *ZXSpectrum(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); + + virtual void set_tape_is_playing(bool is_playing) = 0; + virtual bool get_tape_is_playing() = 0; + + class Options: public Reflection::StructImpl, public Configurable::DisplayOption, public Configurable::QuickloadOption { + friend Configurable::DisplayOption; + friend Configurable::QuickloadOption; + public: + bool automatic_tape_motor_control; + + Options(Configurable::OptionsType type) : + Configurable::DisplayOption(type == Configurable::OptionsType::UserFriendly ? Configurable::Display::RGB : Configurable::Display::CompositeColour), + Configurable::QuickloadOption(type == Configurable::OptionsType::UserFriendly), + automatic_tape_motor_control(type == Configurable::OptionsType::UserFriendly) + { + if(needs_declare()) { + DeclareField(automatic_tape_motor_control); + declare_display_option(); + declare_quickload_option(); + } + } + }; +}; + + +} +} + +#endif /* ZXSpectrum_hpp */ diff --git a/Machines/Utility/MachineForTarget.cpp b/Machines/Utility/MachineForTarget.cpp index c0712421b..9632f0a84 100644 --- a/Machines/Utility/MachineForTarget.cpp +++ b/Machines/Utility/MachineForTarget.cpp @@ -23,7 +23,8 @@ #include "../MasterSystem/MasterSystem.hpp" #include "../MSX/MSX.hpp" #include "../Oric/Oric.hpp" -#include "../ZX8081/ZX8081.hpp" +#include "../Sinclair/ZX8081/ZX8081.hpp" +#include "../Sinclair/ZXSpectrum/ZXSpectrum.hpp" // Sources for construction options. #include "../../Analyser/Static/Acorn/Target.hpp" @@ -38,6 +39,7 @@ #include "../../Analyser/Static/Oric/Target.hpp" #include "../../Analyser/Static/Sega/Target.hpp" #include "../../Analyser/Static/ZX8081/Target.hpp" +#include "../../Analyser/Static/ZXSpectrum/Target.hpp" #include "../../Analyser/Dynamic/MultiMachine/MultiMachine.hpp" #include "TypedDynamicMachine.hpp" @@ -62,7 +64,8 @@ Machine::DynamicMachine *Machine::MachineForTarget(const Analyser::Static::Targe Bind(MSX) Bind(Oric) BindD(Sega::MasterSystem, MasterSystem) - Bind(ZX8081) + BindD(Sinclair::ZX8081, ZX8081) + BindD(Sinclair::ZXSpectrum, ZXSpectrum) default: error = Machine::Error::UnknownMachine; @@ -130,6 +133,7 @@ std::string Machine::ShortNameForTargetMachine(const Analyser::Machine machine) case Analyser::Machine::Oric: return "Oric"; case Analyser::Machine::Vic20: return "Vic20"; case Analyser::Machine::ZX8081: return "ZX8081"; + case Analyser::Machine::ZXSpectrum: return "ZXSpectrum"; default: return ""; } @@ -150,6 +154,7 @@ std::string Machine::LongNameForTargetMachine(Analyser::Machine machine) { case Analyser::Machine::Oric: return "Oric"; case Analyser::Machine::Vic20: return "Vic 20"; case Analyser::Machine::ZX8081: return "ZX80/81"; + case Analyser::Machine::ZXSpectrum: return "ZX Spectrum"; default: return ""; } @@ -200,7 +205,8 @@ std::map> Machine::AllOptionsBy Emplace(MSX, MSX::Machine); Emplace(Oric, Oric::Machine); Emplace(Vic20, Commodore::Vic20::Machine); - Emplace(ZX8081, ZX8081::Machine); + Emplace(ZX8081, Sinclair::ZX8081::Machine); + Emplace(ZXSpectrum, Sinclair::ZXSpectrum::Machine); #undef Emplace @@ -224,6 +230,7 @@ std::map> Machine::Target Add(Oric); AddMapped(Vic20, Commodore); Add(ZX8081); + Add(ZXSpectrum); if(!meaningful_without_media_only) { Add(Atari2600); @@ -232,7 +239,7 @@ std::map> Machine::Target } #undef Add -#undef AddTwo +#undef AddMapped return options; } diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index c671eaf43..afce1bf6c 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -81,9 +81,6 @@ 4B055AD41FAE9B0B0060FFFF /* Oric.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1FA21DADC3DD0039D2E7 /* Oric.cpp */; }; 4B055AD51FAE9B0B0060FFFF /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFDB01DAEF5FF001A68B8 /* Video.cpp */; }; 4B055AD61FAE9B130060FFFF /* MemoryFuzzer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A481F9B8FA70062DABF /* MemoryFuzzer.cpp */; }; - 4B055AD71FAE9B180060FFFF /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0CA1F8D92580050900F /* Keyboard.cpp */; }; - 4B055AD81FAE9B180060FFFF /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD3A3091EE755C800B5B501 /* Video.cpp */; }; - 4B055AD91FAE9B180060FFFF /* ZX8081.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B1497901EE4B5A800CE2596 /* ZX8081.cpp */; }; 4B055ADA1FAE9B460060FFFF /* 1770.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD468F51D8DF41D0084958B /* 1770.cpp */; }; 4B055ADB1FAE9B460060FFFF /* 6560.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC9DF4D1D04691600F44158 /* 6560.cpp */; }; 4B055ADC1FAE9B460060FFFF /* AY38910.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4A762E1DB1A3FA007AAE2E /* AY38910.cpp */; }; @@ -125,6 +122,18 @@ 4B0E04FA1FC9FA3100F43484 /* 9918.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E04F91FC9FA3100F43484 /* 9918.cpp */; }; 4B0E04FB1FC9FA3100F43484 /* 9918.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E04F91FC9FA3100F43484 /* 9918.cpp */; }; 4B0E61071FF34737002A9DBD /* MSX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E61051FF34737002A9DBD /* MSX.cpp */; }; + 4B0F1BB22602645900B85C66 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1BB02602645900B85C66 /* StaticAnalyser.cpp */; }; + 4B0F1BB32602645900B85C66 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1BB02602645900B85C66 /* StaticAnalyser.cpp */; }; + 4B0F1BDA2602FF9800B85C66 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1BCD2602F17B00B85C66 /* Video.cpp */; }; + 4B0F1BDE2602FF9900B85C66 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1BCD2602F17B00B85C66 /* Video.cpp */; }; + 4B0F1BE22602FF9C00B85C66 /* ZX8081.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1BCC2602F17B00B85C66 /* ZX8081.cpp */; }; + 4B0F1BE62602FF9D00B85C66 /* ZX8081.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1BCC2602F17B00B85C66 /* ZX8081.cpp */; }; + 4B0F1BFC260300D900B85C66 /* ZXSpectrum.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1BFA260300D900B85C66 /* ZXSpectrum.cpp */; }; + 4B0F1BFD260300D900B85C66 /* ZXSpectrum.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1BFA260300D900B85C66 /* ZXSpectrum.cpp */; }; + 4B0F1C1C2604EA1000B85C66 /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1C1B2604EA1000B85C66 /* Keyboard.cpp */; }; + 4B0F1C1D2604EA1000B85C66 /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1C1B2604EA1000B85C66 /* Keyboard.cpp */; }; + 4B0F1C232605996900B85C66 /* ZXSpectrumTAP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1C212605996900B85C66 /* ZXSpectrumTAP.cpp */; }; + 4B0F1C242605996900B85C66 /* ZXSpectrumTAP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1C212605996900B85C66 /* ZXSpectrumTAP.cpp */; }; 4B0F94FE208C1A1600FE41D9 /* NIB.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F94FC208C1A1600FE41D9 /* NIB.cpp */; }; 4B0F94FF208C1A1600FE41D9 /* NIB.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F94FC208C1A1600FE41D9 /* NIB.cpp */; }; 4B121F9B1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B121F9A1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm */; }; @@ -133,7 +142,6 @@ 4B1414601B58885000E04248 /* WolfgangLorenzTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B14145F1B58885000E04248 /* WolfgangLorenzTests.swift */; }; 4B1414621B58888700E04248 /* KlausDormannTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1414611B58888700E04248 /* KlausDormannTests.swift */; }; 4B1497881EE4A1DA00CE2596 /* ZX80O81P.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B1497861EE4A1DA00CE2596 /* ZX80O81P.cpp */; }; - 4B1497921EE4B5A800CE2596 /* ZX8081.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B1497901EE4B5A800CE2596 /* ZX8081.cpp */; }; 4B1497981EE4B97F00CE2596 /* ZX8081Options.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B1497961EE4B97F00CE2596 /* ZX8081Options.xib */; }; 4B1558C01F844ECD006E9A97 /* BitReverse.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B1558BE1F844ECD006E9A97 /* BitReverse.cpp */; }; 4B15A9FC208249BB005E6C8D /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B15A9FA208249BB005E6C8D /* StaticAnalyser.cpp */; }; @@ -235,7 +243,6 @@ 4B54C0C21F8D91CD0050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0C11F8D91CD0050900F /* Keyboard.cpp */; }; 4B54C0C51F8D91D90050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0C41F8D91D90050900F /* Keyboard.cpp */; }; 4B54C0C81F8D91E50050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0C61F8D91E50050900F /* Keyboard.cpp */; }; - 4B54C0CB1F8D92590050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0CA1F8D92580050900F /* Keyboard.cpp */; }; 4B55CE5F1C3B7D960093A61B /* MachineDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE5E1C3B7D960093A61B /* MachineDocument.swift */; }; 4B55DD8320DF06680043F2E5 /* MachinePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B55DD8020DF06680043F2E5 /* MachinePicker.swift */; }; 4B55DD8420DF06680043F2E5 /* MachinePicker.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B55DD8120DF06680043F2E5 /* MachinePicker.xib */; }; @@ -892,7 +899,6 @@ 4BD0FBC3233706A200148981 /* CSApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BD0FBC2233706A200148981 /* CSApplication.m */; }; 4BD191F52191180E0042E144 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD191F22191180E0042E144 /* ScanTarget.cpp */; }; 4BD388882239E198002D14B5 /* 68000Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD388872239E198002D14B5 /* 68000Tests.mm */; }; - 4BD3A30B1EE755C800B5B501 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD3A3091EE755C800B5B501 /* Video.cpp */; }; 4BD424E02193B5340097291A /* TextureTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424DD2193B5340097291A /* TextureTarget.cpp */; }; 4BD424E62193B5830097291A /* Shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E12193B5820097291A /* Shader.cpp */; }; 4BD424E82193B5830097291A /* Rectangle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E22193B5820097291A /* Rectangle.cpp */; }; @@ -1055,6 +1061,21 @@ 4B0E04F91FC9FA3100F43484 /* 9918.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = 9918.cpp; path = 9918/9918.cpp; sourceTree = ""; }; 4B0E61051FF34737002A9DBD /* MSX.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MSX.cpp; path = Parsers/MSX.cpp; sourceTree = ""; }; 4B0E61061FF34737002A9DBD /* MSX.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = MSX.hpp; path = Parsers/MSX.hpp; sourceTree = ""; }; + 4B0F1BB02602645900B85C66 /* StaticAnalyser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StaticAnalyser.cpp; sourceTree = ""; }; + 4B0F1BB12602645900B85C66 /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StaticAnalyser.hpp; sourceTree = ""; }; + 4B0F1BCC2602F17B00B85C66 /* ZX8081.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ZX8081.cpp; sourceTree = ""; }; + 4B0F1BCD2602F17B00B85C66 /* Video.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Video.cpp; sourceTree = ""; }; + 4B0F1BCE2602F17B00B85C66 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = ""; }; + 4B0F1BD02602F17B00B85C66 /* ZX8081.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ZX8081.hpp; sourceTree = ""; }; + 4B0F1BFA260300D900B85C66 /* ZXSpectrum.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ZXSpectrum.cpp; sourceTree = ""; }; + 4B0F1BFB260300D900B85C66 /* ZXSpectrum.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ZXSpectrum.hpp; sourceTree = ""; }; + 4B0F1C04260391F100B85C66 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = ""; }; + 4B0F1C092603BA5F00B85C66 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = ""; }; + 4B0F1C1A2604EA1000B85C66 /* Keyboard.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Keyboard.hpp; sourceTree = ""; }; + 4B0F1C1B2604EA1000B85C66 /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Keyboard.cpp; sourceTree = ""; }; + 4B0F1C212605996900B85C66 /* ZXSpectrumTAP.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ZXSpectrumTAP.cpp; sourceTree = ""; }; + 4B0F1C222605996900B85C66 /* ZXSpectrumTAP.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ZXSpectrumTAP.hpp; sourceTree = ""; }; + 4B0F1C3D26095AC600B85C66 /* FDC.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = FDC.hpp; path = AmstradCPC/FDC.hpp; sourceTree = ""; }; 4B0F94FC208C1A1600FE41D9 /* NIB.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = NIB.cpp; sourceTree = ""; }; 4B0F94FD208C1A1600FE41D9 /* NIB.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = NIB.hpp; sourceTree = ""; }; 4B0F9500208C42A300FE41D9 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Target.hpp; path = AppleII/Target.hpp; sourceTree = ""; }; @@ -1068,8 +1089,6 @@ 4B1497871EE4A1DA00CE2596 /* ZX80O81P.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ZX80O81P.hpp; sourceTree = ""; }; 4B14978D1EE4B4D200CE2596 /* CSZX8081.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSZX8081.h; sourceTree = ""; }; 4B14978E1EE4B4D200CE2596 /* CSZX8081.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CSZX8081.mm; sourceTree = ""; }; - 4B1497901EE4B5A800CE2596 /* ZX8081.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ZX8081.cpp; path = ZX8081/ZX8081.cpp; sourceTree = ""; }; - 4B1497911EE4B5A800CE2596 /* ZX8081.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ZX8081.hpp; path = ZX8081/ZX8081.hpp; sourceTree = ""; }; 4B1497971EE4B97F00CE2596 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/ZX8081Options.xib"; sourceTree = SOURCE_ROOT; }; 4B1558BE1F844ECD006E9A97 /* BitReverse.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = BitReverse.cpp; path = Data/BitReverse.cpp; sourceTree = ""; }; 4B1558BF1F844ECD006E9A97 /* BitReverse.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = BitReverse.hpp; path = Data/BitReverse.hpp; sourceTree = ""; }; @@ -1243,8 +1262,6 @@ 4B54C0C41F8D91D90050900F /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Keyboard.cpp; sourceTree = ""; }; 4B54C0C61F8D91E50050900F /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Keyboard.cpp; path = Electron/Keyboard.cpp; sourceTree = ""; }; 4B54C0C71F8D91E50050900F /* Keyboard.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Keyboard.hpp; path = Electron/Keyboard.hpp; sourceTree = ""; }; - 4B54C0C91F8D92580050900F /* Keyboard.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Keyboard.hpp; path = ZX8081/Keyboard.hpp; sourceTree = ""; }; - 4B54C0CA1F8D92580050900F /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Keyboard.cpp; path = ZX8081/Keyboard.cpp; sourceTree = ""; }; 4B55CE5E1C3B7D960093A61B /* MachineDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MachineDocument.swift; sourceTree = ""; }; 4B55DD8020DF06680043F2E5 /* MachinePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MachinePicker.swift; sourceTree = ""; }; 4B55DD8220DF06680043F2E5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MachinePicker.xib; sourceTree = ""; }; @@ -1894,8 +1911,6 @@ 4BD191F32191180E0042E144 /* ScanTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ScanTarget.hpp; sourceTree = ""; }; 4BD388411FE34E010042B588 /* 9918Base.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = 9918Base.hpp; path = 9918/Implementation/9918Base.hpp; sourceTree = ""; }; 4BD388872239E198002D14B5 /* 68000Tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = 68000Tests.mm; sourceTree = ""; }; - 4BD3A3091EE755C800B5B501 /* Video.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Video.cpp; path = ZX8081/Video.cpp; sourceTree = ""; }; - 4BD3A30A1EE755C800B5B501 /* Video.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Video.hpp; path = ZX8081/Video.hpp; sourceTree = ""; }; 4BD424DD2193B5340097291A /* TextureTarget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TextureTarget.cpp; sourceTree = ""; }; 4BD424DE2193B5340097291A /* TextureTarget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TextureTarget.hpp; sourceTree = ""; }; 4BD424E12193B5820097291A /* Shader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Shader.cpp; sourceTree = ""; }; @@ -2154,6 +2169,56 @@ name = 9918; sourceTree = ""; }; + 4B0F1BAF2602645900B85C66 /* ZXSpectrum */ = { + isa = PBXGroup; + children = ( + 4B0F1BB02602645900B85C66 /* StaticAnalyser.cpp */, + 4B0F1BB12602645900B85C66 /* StaticAnalyser.hpp */, + 4B0F1C04260391F100B85C66 /* Target.hpp */, + ); + path = ZXSpectrum; + sourceTree = ""; + }; + 4B0F1BC92602F17B00B85C66 /* Sinclair */ = { + isa = PBXGroup; + children = ( + 4B0F1C192604EA1000B85C66 /* Keyboard */, + 4B0F1BCA2602F17B00B85C66 /* ZX8081 */, + 4B0F1BF9260300D900B85C66 /* ZXSpectrum */, + ); + path = Sinclair; + sourceTree = ""; + }; + 4B0F1BCA2602F17B00B85C66 /* ZX8081 */ = { + isa = PBXGroup; + children = ( + 4B0F1BCC2602F17B00B85C66 /* ZX8081.cpp */, + 4B0F1BCD2602F17B00B85C66 /* Video.cpp */, + 4B0F1BCE2602F17B00B85C66 /* Video.hpp */, + 4B0F1BD02602F17B00B85C66 /* ZX8081.hpp */, + ); + path = ZX8081; + sourceTree = ""; + }; + 4B0F1BF9260300D900B85C66 /* ZXSpectrum */ = { + isa = PBXGroup; + children = ( + 4B0F1BFA260300D900B85C66 /* ZXSpectrum.cpp */, + 4B0F1BFB260300D900B85C66 /* ZXSpectrum.hpp */, + 4B0F1C092603BA5F00B85C66 /* Video.hpp */, + ); + path = ZXSpectrum; + sourceTree = ""; + }; + 4B0F1C192604EA1000B85C66 /* Keyboard */ = { + isa = PBXGroup; + children = ( + 4B0F1C1A2604EA1000B85C66 /* Keyboard.hpp */, + 4B0F1C1B2604EA1000B85C66 /* Keyboard.cpp */, + ); + path = Keyboard; + sourceTree = ""; + }; 4B1414561B58879D00E04248 /* 6502 */ = { isa = PBXGroup; children = ( @@ -2191,19 +2256,6 @@ name = "Test Binaries"; sourceTree = ""; }; - 4B1497931EE4B5AC00CE2596 /* ZX8081 */ = { - isa = PBXGroup; - children = ( - 4B54C0CA1F8D92580050900F /* Keyboard.cpp */, - 4BD3A3091EE755C800B5B501 /* Video.cpp */, - 4B1497901EE4B5A800CE2596 /* ZX8081.cpp */, - 4B54C0C91F8D92580050900F /* Keyboard.hpp */, - 4BD3A30A1EE755C800B5B501 /* Video.hpp */, - 4B1497911EE4B5A800CE2596 /* ZX8081.hpp */, - ); - name = ZX8081; - sourceTree = ""; - }; 4B15A9FE20824C9F005E6C8D /* AppleII */ = { isa = PBXGroup; children = ( @@ -2453,6 +2505,7 @@ 4B54C0C11F8D91CD0050900F /* Keyboard.cpp */, 4B38F3471F2EC11D00D9235D /* AmstradCPC.hpp */, 4B54C0C01F8D91CD0050900F /* Keyboard.hpp */, + 4B0F1C3D26095AC600B85C66 /* FDC.hpp */, ); name = AmstradCPC; sourceTree = ""; @@ -2815,6 +2868,7 @@ 4B69FB421C4D941400B5F0AA /* TapeUEF.cpp */, 4B448E7F1F1C45A00009ABD6 /* TZX.cpp */, 4B1497861EE4A1DA00CE2596 /* ZX80O81P.cpp */, + 4B0F1C212605996900B85C66 /* ZXSpectrumTAP.cpp */, 4B0E04E91FC9E5DA00F43484 /* CAS.hpp */, 4BC91B821D1F160E00884B76 /* CommodoreTAP.hpp */, 4B3BF5AF1F146264005B6C36 /* CSW.hpp */, @@ -2823,6 +2877,7 @@ 4B69FB431C4D941400B5F0AA /* TapeUEF.hpp */, 4B448E801F1C45A00009ABD6 /* TZX.hpp */, 4B1497871EE4A1DA00CE2596 /* ZX80O81P.hpp */, + 4B0F1C222605996900B85C66 /* ZXSpectrumTAP.hpp */, 4B69FB451C4D950F00B5F0AA /* libz.tbd */, ); path = Formats; @@ -3095,6 +3150,7 @@ 4B8944F6201967B4007DE474 /* Oric */, 4B7F1894215486A100388727 /* Sega */, 4B894504201967B4007DE474 /* ZX8081 */, + 4B0F1BAF2602645900B85C66 /* ZXSpectrum */, ); path = Static; sourceTree = ""; @@ -3966,8 +4022,8 @@ 4B7F188B2154825D00388727 /* MasterSystem */, 4B79A4FC1FC8FF9800EEDAD5 /* MSX */, 4BCF1FA51DADC3E10039D2E7 /* Oric */, + 4B0F1BC92602F17B00B85C66 /* Sinclair */, 4B2B3A461F9B8FA70062DABF /* Utility */, - 4B1497931EE4B5AC00CE2596 /* ZX8081 */, ); name = Machines; path = ../../Machines; @@ -5055,7 +5111,6 @@ 4BBB70A9202014E2002FE009 /* MultiProducer.cpp in Sources */, 4B2E86BF25D74F160024F1E9 /* Mouse.cpp in Sources */, 4B6ED2F1208E2F8A0047B343 /* WOZ.cpp in Sources */, - 4B055AD81FAE9B180060FFFF /* Video.cpp in Sources */, 4B5D5C9825F56FC7001B4623 /* Spectrum.cpp in Sources */, 4B2E86D025D8D8C70024F1E9 /* Keyboard.cpp in Sources */, 4B89452F201967B4007DE474 /* StaticAnalyser.cpp in Sources */, @@ -5071,8 +5126,9 @@ 4B2BF19123DCC6A200C3AD60 /* BD500.cpp in Sources */, 4B055ADA1FAE9B460060FFFF /* 1770.cpp in Sources */, 4B80CD77256CA16600176FCC /* 2MG.cpp in Sources */, + 4B0F1BE62602FF9D00B85C66 /* ZX8081.cpp in Sources */, + 4B0F1BDE2602FF9900B85C66 /* Video.cpp in Sources */, 4B055ADC1FAE9B460060FFFF /* AY38910.cpp in Sources */, - 4B055AD71FAE9B180060FFFF /* Keyboard.cpp in Sources */, 4BD67DCC209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */, 4B055AB61FAE860F0060FFFF /* TapeUEF.cpp in Sources */, 4B055A9D1FAE85DA0060FFFF /* D64.cpp in Sources */, @@ -5100,6 +5156,7 @@ 4B055A9E1FAE85DA0060FFFF /* G64.cpp in Sources */, 4B055AB81FAE860F0060FFFF /* ZX80O81P.cpp in Sources */, 4B055AB01FAE86070060FFFF /* PulseQueuedTape.cpp in Sources */, + 4B0F1C1D2604EA1000B85C66 /* Keyboard.cpp in Sources */, 4B055AAC1FAE85FD0060FFFF /* PCMSegment.cpp in Sources */, 4BB307BC235001C300457D33 /* 6850.cpp in Sources */, 4B055AB31FAE860F0060FFFF /* CSW.cpp in Sources */, @@ -5189,6 +5246,7 @@ 4B894525201967B4007DE474 /* Tape.cpp in Sources */, 4B055ACD1FAE9B030060FFFF /* Keyboard.cpp in Sources */, 4B055AB21FAE860F0060FFFF /* CommodoreTAP.cpp in Sources */, + 4B0F1BFD260300D900B85C66 /* ZXSpectrum.cpp in Sources */, 4B055ADF1FAE9B4C0060FFFF /* IRQDelegatePortHandler.cpp in Sources */, 4B74CF86231370BC00500CE8 /* MacintoshVolume.cpp in Sources */, 4B0ACC3323775819008902D0 /* Atari2600.cpp in Sources */, @@ -5219,13 +5277,14 @@ 4BEDA3C125B25563000C2DBD /* Decoder.cpp in Sources */, 4BB0A65C2044FD3000FB3688 /* SN76489.cpp in Sources */, 4B595FAE2086DFBA0083CAA8 /* AudioToggle.cpp in Sources */, + 4B0F1C242605996900B85C66 /* ZXSpectrumTAP.cpp in Sources */, 4B055AB91FAE86170060FFFF /* Acorn.cpp in Sources */, 4B302185208A550100773308 /* DiskII.cpp in Sources */, + 4B0F1BB32602645900B85C66 /* StaticAnalyser.cpp in Sources */, 4B055A931FAE85B50060FFFF /* BinaryDump.cpp in Sources */, 4B89452D201967B4007DE474 /* Tape.cpp in Sources */, 4B055AD61FAE9B130060FFFF /* MemoryFuzzer.cpp in Sources */, 4B055AC21FAE9AE30060FFFF /* KeyboardMachine.cpp in Sources */, - 4B055AD91FAE9B180060FFFF /* ZX8081.cpp in Sources */, 4B89453B201967B4007DE474 /* StaticAnalyser.cpp in Sources */, 4B055AEB1FAE9BA20060FFFF /* PartialMachineCycle.cpp in Sources */, ); @@ -5249,6 +5308,7 @@ 4B0E04EA1FC9E5DA00F43484 /* CAS.cpp in Sources */, 4B7A90ED20410A85008514A2 /* StaticAnalyser.cpp in Sources */, 4B58601E1F806AB200AEE2E3 /* MFMSectorDump.cpp in Sources */, + 4B0F1C1C2604EA1000B85C66 /* Keyboard.cpp in Sources */, 4B228CD924DA12C60077EF25 /* CSScanTargetView.m in Sources */, 4B6AAEAD230E40250078E864 /* Target.cpp in Sources */, 4B448E841F1C4C480009ABD6 /* PulseQueuedTape.cpp in Sources */, @@ -5272,17 +5332,16 @@ 4BB4BFB922A4372F0069048D /* StaticAnalyser.cpp in Sources */, 4B9BE400203A0C0600FFAE60 /* MultiSpeaker.cpp in Sources */, 4B894538201967B4007DE474 /* Tape.cpp in Sources */, - 4B54C0CB1F8D92590050900F /* Keyboard.cpp in Sources */, 4BEDA43025B3C700000C2DBD /* Executor.cpp in Sources */, 4B1B58F6246CC4E8009C171E /* State.cpp in Sources */, 4BEA525E1DF33323007E74F2 /* Tape.cpp in Sources */, 4B2BF19623E10F0100C3AD60 /* CSHighPrecisionTimer.m in Sources */, 4BE211FF253FC80900435408 /* StaticAnalyser.cpp in Sources */, + 4B0F1BE22602FF9C00B85C66 /* ZX8081.cpp in Sources */, 4B8334951F5E25B60097E338 /* C1540.cpp in Sources */, 4BEDA40C25B2844B000C2DBD /* Decoder.cpp in Sources */, 4B89453C201967B4007DE474 /* StaticAnalyser.cpp in Sources */, 4B595FAD2086DFBA0083CAA8 /* AudioToggle.cpp in Sources */, - 4B1497921EE4B5A800CE2596 /* ZX8081.cpp in Sources */, 4B2E86BE25D74F160024F1E9 /* Mouse.cpp in Sources */, 4B643F3F1D77B88000D431D6 /* DocumentController.swift in Sources */, 4BDA00E422E663B900AC3CD0 /* NSData+CRC32.m in Sources */, @@ -5326,6 +5385,7 @@ 4B0ACC2E23775819008902D0 /* TIA.cpp in Sources */, 4B2E86B725D7490E0024F1E9 /* ReactiveDevice.cpp in Sources */, 4B74CF85231370BC00500CE8 /* MacintoshVolume.cpp in Sources */, + 4B0F1C232605996900B85C66 /* ZXSpectrumTAP.cpp in Sources */, 4B4518A51F75FD1C00926311 /* SSD.cpp in Sources */, 4B55CE5F1C3B7D960093A61B /* MachineDocument.swift in Sources */, 4B1B58FF246E19FD009C171E /* State.cpp in Sources */, @@ -5373,7 +5433,6 @@ 4BD468F71D8DF41D0084958B /* 1770.cpp in Sources */, 4B7F1897215486A200388727 /* StaticAnalyser.cpp in Sources */, 4B47F6C5241C87A100ED06F7 /* Struct.cpp in Sources */, - 4BD3A30B1EE755C800B5B501 /* Video.cpp in Sources */, 4B5FADBA1DE3151600AEC565 /* FileHolder.cpp in Sources */, 4B643F3A1D77AD1900D431D6 /* CSStaticAnalyser.mm in Sources */, 4B622AE5222E0AD5008B59F2 /* DisplayMetrics.cpp in Sources */, @@ -5408,6 +5467,7 @@ 4B7F188E2154825E00388727 /* MasterSystem.cpp in Sources */, 4B8805F41DCFD22A003085B1 /* Commodore.cpp in Sources */, 4B3FCC40201EC24200960631 /* MultiMachine.cpp in Sources */, + 4B0F1BB22602645900B85C66 /* StaticAnalyser.cpp in Sources */, 4B8805F01DCFC99C003085B1 /* Acorn.cpp in Sources */, 4B3051301D98ACC600B4FED8 /* Plus3.cpp in Sources */, 4B30512D1D989E2200B4FED8 /* Drive.cpp in Sources */, @@ -5431,6 +5491,7 @@ 4B6ED2F0208E2F8A0047B343 /* WOZ.cpp in Sources */, 4B15A9FC208249BB005E6C8D /* StaticAnalyser.cpp in Sources */, 4B5FADC01DE3BF2B00AEC565 /* Microdisc.cpp in Sources */, + 4B0F1BDA2602FF9800B85C66 /* Video.cpp in Sources */, 4B54C0C81F8D91E50050900F /* Keyboard.cpp in Sources */, 4B79A5011FC913C900EEDAD5 /* MSX.cpp in Sources */, 4BEE0A701D72496600532C7B /* PRG.cpp in Sources */, @@ -5440,6 +5501,7 @@ 4B8334861F5DA3780097E338 /* 6502Storage.cpp in Sources */, 4B8FE2271DA1DE2D0090D3CE /* NSBundle+DataResource.m in Sources */, 4BC91B831D1F160E00884B76 /* CommodoreTAP.cpp in Sources */, + 4B0F1BFC260300D900B85C66 /* ZXSpectrum.cpp in Sources */, 4B55DD8320DF06680043F2E5 /* MachinePicker.swift in Sources */, 4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */, 4B89453E201967B4007DE474 /* StaticAnalyser.cpp in Sources */, diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h index 964397320..1193281f4 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h @@ -62,6 +62,11 @@ typedef NS_ENUM(NSInteger, CSMachineOricDiskInterface) { CSMachineOricDiskInterfaceBD500 }; +typedef NS_ENUM(NSInteger, CSMachineSpectrumModel) { + CSMachineSpectrumModelPlus2a, + CSMachineSpectrumModelPlus3, +}; + typedef NS_ENUM(NSInteger, CSMachineVic20Region) { CSMachineVic20RegionAmerican, CSMachineVic20RegionEuropean, @@ -90,6 +95,7 @@ typedef int Kilobytes; - (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model; - (instancetype)initWithMSXRegion:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive; - (instancetype)initWithOricModel:(CSMachineOricModel)model diskInterface:(CSMachineOricDiskInterface)diskInterface; +- (instancetype)initWithSpectrumModel:(CSMachineSpectrumModel)model; - (instancetype)initWithVic20Region:(CSMachineVic20Region)region memorySize:(Kilobytes)memorySize hasC1540:(BOOL)hasC1540; - (instancetype)initWithZX80MemorySize:(Kilobytes)memorySize useZX81ROM:(BOOL)useZX81ROM; - (instancetype)initWithZX81MemorySize:(Kilobytes)memorySize; diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm index 48950edca..993a5c047 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm @@ -23,6 +23,7 @@ #include "../../../../../Analyser/Static/MSX/Target.hpp" #include "../../../../../Analyser/Static/Oric/Target.hpp" #include "../../../../../Analyser/Static/ZX8081/Target.hpp" +#include "../../../../../Analyser/Static/ZXSpectrum/Target.hpp" #import "Clock_Signal-Swift.h" @@ -187,6 +188,20 @@ return self; } +- (instancetype)initWithSpectrumModel:(CSMachineSpectrumModel)model { + self = [super init]; + if(self) { + using Target = Analyser::Static::ZXSpectrum::Target; + auto target = std::make_unique(); + switch(model) { + case CSMachineSpectrumModelPlus2a: target->model = Target::Model::Plus2a; break; + case CSMachineSpectrumModelPlus3: target->model = Target::Model::Plus3; break; + } + _targets.push_back(std::move(target)); + } + return self; +} + - (instancetype)initWithVic20Region:(CSMachineVic20Region)region memorySize:(Kilobytes)memorySize hasC1540:(BOOL)hasC1540 { self = [super init]; if(self) { @@ -263,6 +278,7 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K case Analyser::Machine::Oric: return @"OricOptions"; case Analyser::Machine::Vic20: return @"QuickLoadCompositeOptions"; case Analyser::Machine::ZX8081: return @"ZX8081Options"; + case Analyser::Machine::ZXSpectrum: return @"QuickLoadCompositeOptions"; // TODO: @"ZXSpectrumOptions"; default: return nil; } } diff --git a/OSBindings/Mac/Clock Signal/Machine/Wrappers/CSZX8081.mm b/OSBindings/Mac/Clock Signal/Machine/Wrappers/CSZX8081.mm index 80b8fb822..cba3ec1fa 100644 --- a/OSBindings/Mac/Clock Signal/Machine/Wrappers/CSZX8081.mm +++ b/OSBindings/Mac/Clock Signal/Machine/Wrappers/CSZX8081.mm @@ -11,14 +11,14 @@ #include "ZX8081.hpp" @implementation CSZX8081 { - ZX8081::Machine *_zx8081; + Sinclair::ZX8081::Machine *_zx8081; __weak CSMachine *_machine; } - (instancetype)initWithZX8081:(void *)zx8081 owner:(CSMachine *)machine { self = [super init]; if(self) { - _zx8081 = (ZX8081::Machine *)zx8081; + _zx8081 = (Sinclair::ZX8081::Machine *)zx8081; _machine = machine; } return self; diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib index e8579466d..d233186dc 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib +++ b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib @@ -17,14 +17,14 @@ - - + + - +