1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-06-18 13:29:41 +00:00

Merge pull request #883 from TomHarte/ZXSpectrum

Adds the ZX Spectrum +2a and +3 as emulated machines.
This commit is contained in:
Thomas Harte 2021-03-22 23:28:04 -04:00 committed by GitHub
commit 04291e9a86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 2031 additions and 213 deletions

View File

@ -24,7 +24,8 @@ enum class Machine {
MSX,
Oric,
Vic20,
ZX8081
ZX8081,
ZXSpectrum,
};
}

View File

@ -11,12 +11,12 @@
#include <algorithm>
#include <cstring>
#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) {

View File

@ -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<Storage::Disk::D64>, 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<Storage::Disk::CPCDSK>,
TargetPlatform::AmstradCPC | TargetPlatform::Oric) // DSK (Amstrad CPC)
TargetPlatform::AmstradCPC | TargetPlatform::Oric | TargetPlatform::ZXSpectrum) // DSK (Amstrad CPC, etc)
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DSK (Apple II)
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // DSK (Macintosh, floppy disk)
Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk)
@ -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<Storage::Disk::SSD>, TargetPlatform::Acorn) // SSD
@ -176,8 +178,9 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
Format("stx", result.disks, Disk::DiskImageHolder<Storage::Disk::STX>, 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<Storage::Disk::WOZ>, 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();

View File

@ -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<Storage::Tape::Tape> &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<Storage::Disk::Disk> &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>();
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;
}

View File

@ -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 <string>
namespace Analyser {
namespace Static {
namespace ZXSpectrum {
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
}
}
}
#endif /* StaticAnalyser_hpp */

View File

@ -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<Target> {
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 */

View File

@ -164,6 +164,31 @@ template <bool is_stereo> 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 <typename AY> static void select_register(AY &ay, uint8_t reg) {
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | GI::AY38910::BC1));
ay.set_data_input(reg);
ay.set_control_lines(GI::AY38910::ControlLines(0));
}
template <typename AY> static void write_data(AY &ay, uint8_t reg) {
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2));
ay.set_data_input(reg);
ay.set_control_lines(GI::AY38910::ControlLines(0));
}
template <typename AY> 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;
}
};
}
}

View File

@ -12,7 +12,7 @@
#include <limits>
/*!
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;

View File

@ -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<Storage::Disk::Disk> 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 <bool has_fdc> class ConcreteMachine:
i8255PortHandler i8255_port_handler_;
Intel::i8255::i8255<i8255PortHandler> i8255_;
FDC fdc_;
Amstrad::FDC fdc_;
HalfCycles time_since_fdc_update_;
void flush_fdc() {
if constexpr (has_fdc) {

View File

@ -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<int>(), 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<Storage::Disk::Disk> 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 */

View File

@ -657,7 +657,6 @@ class ConcreteMachine:
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
const auto options = dynamic_cast<Options *>(str.get());
set_video_signal_configurable(options->output);
allow_fast_tape_ = options->quickload;
set_use_fast_tape();

View File

@ -8,10 +8,14 @@
#include "Keyboard.hpp"
using namespace ZX8081;
#include <cstring>
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;
}

View File

@ -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 */

View File

@ -10,7 +10,7 @@
#include <algorithm>
using namespace ZX8081;
using namespace Sinclair::ZX8081;
namespace {

View File

@ -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 */

View File

@ -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 <cstdint>
@ -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<bool is_zx81> class ConcreteMachine:
public MachineTypes::TimedMachine,
public MachineTypes::ScanProducer,
@ -60,14 +63,15 @@ template<bool is_zx81> class ConcreteMachine:
public Machine {
public:
ConcreteMachine(const Analyser::Static::ZX8081::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
Utility::TypeRecipient<CharacterMapper>(is_zx81),
Utility::TypeRecipient<CharacterMapper>(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<bool is_zx81> 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<bool is_zx81> 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<bool is_zx81> class ConcreteMachine:
}
// MARK: - Configuration options.
std::unique_ptr<Reflection::Struct> get_options() final {
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly); // OptionsType is arbitrary, but not optional.
options->automatic_tape_motor_control = use_automatic_tape_motor_control_;
@ -447,8 +426,8 @@ template<bool is_zx81> 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<Storage::Tape::BinaryTapePlayer> tape_player_;
Storage::Tape::ZX8081::Parser parser_;
@ -515,17 +494,18 @@ template<bool is_zx81> 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<const Analyser::Static::ZX8081::Target *>(target);
const auto zx_target = dynamic_cast<const Analyser::Static::ZX8081::Target *>(target);
// Instantiate the correct type of machine.
if(zx_target->is_ZX81) return new ZX8081::ConcreteMachine<true>(*zx_target, rom_fetcher);
else return new ZX8081::ConcreteMachine<false>(*zx_target, rom_fetcher);
if(zx_target->is_ZX81) return new ConcreteMachine<true>(*zx_target, rom_fetcher);
else return new ConcreteMachine<false>(*zx_target, rom_fetcher);
}
Machine::~Machine() {}

View File

@ -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 <memory>
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 */

View File

@ -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 <algorithm>
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 <VideoTiming timing> 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 1436514368 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<int>();
while(cycles_remaining) {
int line = time_into_frame_ / timings.cycles_per_line;
int offset = time_into_frame_ % timings.cycles_per_line;
const int cycles_this_line = std::min(cycles_remaining, timings.cycles_per_line - offset);
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<int>() + 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 */

View File

@ -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 <array>
namespace Sinclair {
namespace ZXSpectrum {
using Model = Analyser::Static::ZXSpectrum::Target::Model;
template<Model model> 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);
// b0b2: 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: b0b4: 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<Reflection::Struct> get_options() override {
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly); // OptionsType is arbitrary, but not optional.
options->automatic_tape_motor_control = use_automatic_tape_motor_control_;
options->quickload = allow_fast_tape_hack_;
return options;
}
void set_options(const std::unique_ptr<Reflection::Struct> &str) override {
const auto options = dynamic_cast<Options *>(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<ConcreteMachine, false, false> z80_;
// MARK: - Memory.
std::array<uint8_t, 64*1024> rom_;
std::array<uint8_t, 128*1024> ram_;
std::array<uint8_t, 16*1024> 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<false> ay_;
Audio::Toggle audio_toggle_;
Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle> mixer_;
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, 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<video_timing>> 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<Amstrad::FDC, 1, 1, Cycles> 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<const Analyser::Static::ZXSpectrum::Target *>(target);
switch(zx_target->model) {
case Model::Plus2a: return new ConcreteMachine<Model::Plus2a>(*zx_target, rom_fetcher);
case Model::Plus3: return new ConcreteMachine<Model::Plus3>(*zx_target, rom_fetcher);
}
return nullptr;
}
Machine::~Machine() {}

View File

@ -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 <memory>
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<Options>, public Configurable::DisplayOption<Options>, public Configurable::QuickloadOption<Options> {
friend Configurable::DisplayOption<Options>;
friend Configurable::QuickloadOption<Options>;
public:
bool automatic_tape_motor_control;
Options(Configurable::OptionsType type) :
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ? Configurable::Display::RGB : Configurable::Display::CompositeColour),
Configurable::QuickloadOption<Options>(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 */

View File

@ -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<std::string, std::unique_ptr<Reflection::Struct>> 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<std::string, std::unique_ptr<Analyser::Static::Target>> Machine::Target
Add(Oric);
AddMapped(Vic20, Commodore);
Add(ZX8081);
Add(ZXSpectrum);
if(!meaningful_without_media_only) {
Add(Atari2600);
@ -232,7 +239,7 @@ std::map<std::string, std::unique_ptr<Analyser::Static::Target>> Machine::Target
}
#undef Add
#undef AddTwo
#undef AddMapped
return options;
}

View File

@ -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 = "<group>"; };
4B0E61051FF34737002A9DBD /* MSX.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MSX.cpp; path = Parsers/MSX.cpp; sourceTree = "<group>"; };
4B0E61061FF34737002A9DBD /* MSX.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = MSX.hpp; path = Parsers/MSX.hpp; sourceTree = "<group>"; };
4B0F1BB02602645900B85C66 /* StaticAnalyser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StaticAnalyser.cpp; sourceTree = "<group>"; };
4B0F1BB12602645900B85C66 /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StaticAnalyser.hpp; sourceTree = "<group>"; };
4B0F1BCC2602F17B00B85C66 /* ZX8081.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ZX8081.cpp; sourceTree = "<group>"; };
4B0F1BCD2602F17B00B85C66 /* Video.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Video.cpp; sourceTree = "<group>"; };
4B0F1BCE2602F17B00B85C66 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = "<group>"; };
4B0F1BD02602F17B00B85C66 /* ZX8081.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ZX8081.hpp; sourceTree = "<group>"; };
4B0F1BFA260300D900B85C66 /* ZXSpectrum.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ZXSpectrum.cpp; sourceTree = "<group>"; };
4B0F1BFB260300D900B85C66 /* ZXSpectrum.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ZXSpectrum.hpp; sourceTree = "<group>"; };
4B0F1C04260391F100B85C66 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = "<group>"; };
4B0F1C092603BA5F00B85C66 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = "<group>"; };
4B0F1C1A2604EA1000B85C66 /* Keyboard.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Keyboard.hpp; sourceTree = "<group>"; };
4B0F1C1B2604EA1000B85C66 /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Keyboard.cpp; sourceTree = "<group>"; };
4B0F1C212605996900B85C66 /* ZXSpectrumTAP.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ZXSpectrumTAP.cpp; sourceTree = "<group>"; };
4B0F1C222605996900B85C66 /* ZXSpectrumTAP.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ZXSpectrumTAP.hpp; sourceTree = "<group>"; };
4B0F1C3D26095AC600B85C66 /* FDC.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = FDC.hpp; path = AmstradCPC/FDC.hpp; sourceTree = "<group>"; };
4B0F94FC208C1A1600FE41D9 /* NIB.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = NIB.cpp; sourceTree = "<group>"; };
4B0F94FD208C1A1600FE41D9 /* NIB.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = NIB.hpp; sourceTree = "<group>"; };
4B0F9500208C42A300FE41D9 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Target.hpp; path = AppleII/Target.hpp; sourceTree = "<group>"; };
@ -1068,8 +1089,6 @@
4B1497871EE4A1DA00CE2596 /* ZX80O81P.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ZX80O81P.hpp; sourceTree = "<group>"; };
4B14978D1EE4B4D200CE2596 /* CSZX8081.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSZX8081.h; sourceTree = "<group>"; };
4B14978E1EE4B4D200CE2596 /* CSZX8081.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CSZX8081.mm; sourceTree = "<group>"; };
4B1497901EE4B5A800CE2596 /* ZX8081.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ZX8081.cpp; path = ZX8081/ZX8081.cpp; sourceTree = "<group>"; };
4B1497911EE4B5A800CE2596 /* ZX8081.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ZX8081.hpp; path = ZX8081/ZX8081.hpp; sourceTree = "<group>"; };
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 = "<group>"; };
4B1558BF1F844ECD006E9A97 /* BitReverse.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = BitReverse.hpp; path = Data/BitReverse.hpp; sourceTree = "<group>"; };
@ -1243,8 +1262,6 @@
4B54C0C41F8D91D90050900F /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Keyboard.cpp; sourceTree = "<group>"; };
4B54C0C61F8D91E50050900F /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Keyboard.cpp; path = Electron/Keyboard.cpp; sourceTree = "<group>"; };
4B54C0C71F8D91E50050900F /* Keyboard.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Keyboard.hpp; path = Electron/Keyboard.hpp; sourceTree = "<group>"; };
4B54C0C91F8D92580050900F /* Keyboard.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Keyboard.hpp; path = ZX8081/Keyboard.hpp; sourceTree = "<group>"; };
4B54C0CA1F8D92580050900F /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Keyboard.cpp; path = ZX8081/Keyboard.cpp; sourceTree = "<group>"; };
4B55CE5E1C3B7D960093A61B /* MachineDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MachineDocument.swift; sourceTree = "<group>"; };
4B55DD8020DF06680043F2E5 /* MachinePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MachinePicker.swift; sourceTree = "<group>"; };
4B55DD8220DF06680043F2E5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MachinePicker.xib; sourceTree = "<group>"; };
@ -1894,8 +1911,6 @@
4BD191F32191180E0042E144 /* ScanTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ScanTarget.hpp; sourceTree = "<group>"; };
4BD388411FE34E010042B588 /* 9918Base.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = 9918Base.hpp; path = 9918/Implementation/9918Base.hpp; sourceTree = "<group>"; };
4BD388872239E198002D14B5 /* 68000Tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = 68000Tests.mm; sourceTree = "<group>"; };
4BD3A3091EE755C800B5B501 /* Video.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Video.cpp; path = ZX8081/Video.cpp; sourceTree = "<group>"; };
4BD3A30A1EE755C800B5B501 /* Video.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Video.hpp; path = ZX8081/Video.hpp; sourceTree = "<group>"; };
4BD424DD2193B5340097291A /* TextureTarget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TextureTarget.cpp; sourceTree = "<group>"; };
4BD424DE2193B5340097291A /* TextureTarget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TextureTarget.hpp; sourceTree = "<group>"; };
4BD424E12193B5820097291A /* Shader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Shader.cpp; sourceTree = "<group>"; };
@ -2154,6 +2169,56 @@
name = 9918;
sourceTree = "<group>";
};
4B0F1BAF2602645900B85C66 /* ZXSpectrum */ = {
isa = PBXGroup;
children = (
4B0F1BB02602645900B85C66 /* StaticAnalyser.cpp */,
4B0F1BB12602645900B85C66 /* StaticAnalyser.hpp */,
4B0F1C04260391F100B85C66 /* Target.hpp */,
);
path = ZXSpectrum;
sourceTree = "<group>";
};
4B0F1BC92602F17B00B85C66 /* Sinclair */ = {
isa = PBXGroup;
children = (
4B0F1C192604EA1000B85C66 /* Keyboard */,
4B0F1BCA2602F17B00B85C66 /* ZX8081 */,
4B0F1BF9260300D900B85C66 /* ZXSpectrum */,
);
path = Sinclair;
sourceTree = "<group>";
};
4B0F1BCA2602F17B00B85C66 /* ZX8081 */ = {
isa = PBXGroup;
children = (
4B0F1BCC2602F17B00B85C66 /* ZX8081.cpp */,
4B0F1BCD2602F17B00B85C66 /* Video.cpp */,
4B0F1BCE2602F17B00B85C66 /* Video.hpp */,
4B0F1BD02602F17B00B85C66 /* ZX8081.hpp */,
);
path = ZX8081;
sourceTree = "<group>";
};
4B0F1BF9260300D900B85C66 /* ZXSpectrum */ = {
isa = PBXGroup;
children = (
4B0F1BFA260300D900B85C66 /* ZXSpectrum.cpp */,
4B0F1BFB260300D900B85C66 /* ZXSpectrum.hpp */,
4B0F1C092603BA5F00B85C66 /* Video.hpp */,
);
path = ZXSpectrum;
sourceTree = "<group>";
};
4B0F1C192604EA1000B85C66 /* Keyboard */ = {
isa = PBXGroup;
children = (
4B0F1C1A2604EA1000B85C66 /* Keyboard.hpp */,
4B0F1C1B2604EA1000B85C66 /* Keyboard.cpp */,
);
path = Keyboard;
sourceTree = "<group>";
};
4B1414561B58879D00E04248 /* 6502 */ = {
isa = PBXGroup;
children = (
@ -2191,19 +2256,6 @@
name = "Test Binaries";
sourceTree = "<group>";
};
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 = "<group>";
};
4B15A9FE20824C9F005E6C8D /* AppleII */ = {
isa = PBXGroup;
children = (
@ -2453,6 +2505,7 @@
4B54C0C11F8D91CD0050900F /* Keyboard.cpp */,
4B38F3471F2EC11D00D9235D /* AmstradCPC.hpp */,
4B54C0C01F8D91CD0050900F /* Keyboard.hpp */,
4B0F1C3D26095AC600B85C66 /* FDC.hpp */,
);
name = AmstradCPC;
sourceTree = "<group>";
@ -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 = "<group>";
@ -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 */,

View File

@ -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;

View File

@ -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<Target>();
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;
}
}

View File

@ -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;

View File

@ -17,14 +17,14 @@
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" titleVisibility="hidden" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" documentModal="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="810" height="205"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1440"/>
<rect key="contentRect" x="196" y="240" width="881" height="205"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="810" height="205"/>
<rect key="frame" x="0.0" y="0.0" width="881" height="205"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hKn-1l-OSN">
<rect key="frame" x="716" y="13" width="81" height="32"/>
<rect key="frame" x="787" y="13" width="81" height="32"/>
<buttonCell key="cell" type="push" title="Choose" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="MnM-xo-4Qa">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@ -37,7 +37,7 @@ DQ
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JQy-Cj-AOK">
<rect key="frame" x="643" y="13" width="76" height="32"/>
<rect key="frame" x="714" y="13" width="76" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="sub-rB-Req">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@ -59,12 +59,12 @@ Gw
</textFieldCell>
</textField>
<tabView translatesAutoresizingMaskIntoConstraints="NO" id="VUb-QG-x7c">
<rect key="frame" x="13" y="50" width="784" height="141"/>
<rect key="frame" x="13" y="50" width="855" height="141"/>
<font key="font" metaFont="system"/>
<tabViewItems>
<tabViewItem label="Apple II" identifier="appleii" id="P59-QG-LOa">
<view key="view" id="dHz-Yv-GNq">
<rect key="frame" x="10" y="33" width="764" height="95"/>
<rect key="frame" x="10" y="33" width="835" height="95"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="V5Z-dX-Ns4">
@ -195,7 +195,7 @@ Gw
</tabViewItem>
<tabViewItem label="Amstrad CPC" identifier="cpc" id="JmB-OF-xcM">
<view key="view" id="5zS-Nj-Ynx">
<rect key="frame" x="10" y="33" width="764" height="95"/>
<rect key="frame" x="10" y="33" width="835" height="95"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="00d-sg-Krh">
@ -457,9 +457,46 @@ Gw
</constraints>
</view>
</tabViewItem>
<tabViewItem label="Spectrum" identifier="spectrum" id="HQv-oF-k8b">
<view key="view" id="bMx-F6-JUb">
<rect key="frame" x="10" y="33" width="835" height="95"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gFZ-d4-WFv">
<rect key="frame" x="63" y="66" width="62" height="25"/>
<popUpButtonCell key="cell" type="push" title="+2a" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="21" imageScaling="axesIndependently" inset="2" selectedItem="Fo7-NL-Kv5" id="tYs-sA-oek">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="8lt-dk-zPr">
<items>
<menuItem title="+2a" state="on" tag="21" id="Fo7-NL-Kv5"/>
<menuItem title="+3" tag="3" id="jwx-fZ-vXp"/>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJ3-ma-Byy">
<rect key="frame" x="18" y="72" width="42" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Model" id="JId-Tp-LrE">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="gFZ-d4-WFv" secondAttribute="trailing" constant="20" symbolic="YES" id="90E-uI-MQg"/>
<constraint firstItem="fJ3-ma-Byy" firstAttribute="leading" secondItem="bMx-F6-JUb" secondAttribute="leading" constant="20" symbolic="YES" id="9kE-iQ-dxd"/>
<constraint firstItem="fJ3-ma-Byy" firstAttribute="centerY" secondItem="gFZ-d4-WFv" secondAttribute="centerY" id="LxG-5E-Q5Y"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="gFZ-d4-WFv" secondAttribute="bottom" constant="5" id="d8S-vX-B5e"/>
<constraint firstItem="gFZ-d4-WFv" firstAttribute="leading" secondItem="fJ3-ma-Byy" secondAttribute="trailing" constant="8" symbolic="YES" id="hKS-47-R2y"/>
<constraint firstItem="gFZ-d4-WFv" firstAttribute="top" secondItem="bMx-F6-JUb" secondAttribute="top" constant="5" id="wsX-Wq-iPt"/>
</constraints>
</view>
</tabViewItem>
<tabViewItem label="Vic-20" identifier="vic20" id="cyO-PU-hSU">
<view key="view" id="fLI-XB-QCr">
<rect key="frame" x="10" y="33" width="764" height="95"/>
<rect key="frame" x="10" y="33" width="819" height="95"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ueK-gq-gaF">
@ -638,7 +675,7 @@ Gw
<constraint firstItem="VUb-QG-x7c" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" constant="20" id="zT3-Ea-QQJ"/>
</constraints>
</view>
<point key="canvasLocation" x="94.5" y="90.5"/>
<point key="canvasLocation" x="129.5" y="90.5"/>
</window>
<customObject id="192-Eb-Rpg" customClass="MachinePicker" customModule="Clock_Signal" customModuleProvider="target">
<connections>
@ -658,6 +695,7 @@ Gw
<outlet property="msxRegionButton" destination="LG6-mP-SeG" id="3a9-VG-6Wf"/>
<outlet property="oricDiskInterfaceButton" destination="fYL-p6-wyn" id="aAt-wM-hRZ"/>
<outlet property="oricModelTypeButton" destination="ENP-hI-BVZ" id="n9i-Ym-miE"/>
<outlet property="spectrumModelTypeButton" destination="gFZ-d4-WFv" id="tdX-Cv-Swe"/>
<outlet property="vic20HasC1540Button" destination="Lrf-gL-6EI" id="21g-dJ-mOo"/>
<outlet property="vic20MemorySizeButton" destination="2eV-Us-eEv" id="5j4-jw-89d"/>
<outlet property="vic20RegionButton" destination="ueK-gq-gaF" id="qMq-uP-y4r"/>

View File

@ -40,6 +40,9 @@ class MachinePicker: NSObject {
@IBOutlet var oricModelTypeButton: NSPopUpButton!
@IBOutlet var oricDiskInterfaceButton: NSPopUpButton!
// MARK: - Spectrum properties
@IBOutlet var spectrumModelTypeButton: NSPopUpButton!
// MARK: - Vic-20 properties
@IBOutlet var vic20RegionButton: NSPopUpButton!
@IBOutlet var vic20MemorySizeButton: NSPopUpButton!
@ -98,6 +101,9 @@ class MachinePicker: NSObject {
oricDiskInterfaceButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.oricDiskInterface"))
oricModelTypeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.oricModel"))
// Spectrum settings
spectrumModelTypeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.spectrumModel"))
// Vic-20 settings
vic20RegionButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.vic20Region"))
vic20MemorySizeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.vic20MemorySize"))
@ -145,6 +151,9 @@ class MachinePicker: NSObject {
standardUserDefaults.set(oricDiskInterfaceButton.selectedTag(), forKey: "new.oricDiskInterface")
standardUserDefaults.set(oricModelTypeButton.selectedTag(), forKey: "new.oricModel")
// Spectrum settings
standardUserDefaults.set(spectrumModelTypeButton.selectedTag(), forKey: "new.spectrumModel")
// Vic-20 settings
standardUserDefaults.set(vic20RegionButton.selectedTag(), forKey: "new.vic20Region")
standardUserDefaults.set(vic20MemorySizeButton.selectedTag(), forKey: "new.vic20MemorySize")
@ -241,18 +250,28 @@ class MachinePicker: NSObject {
case 2: diskInterface = .pravetz
case 3: diskInterface = .jasmin
case 4: diskInterface = .BD500
default: break;
default: break
}
var model: CSMachineOricModel = .oric1
switch oricModelTypeButton.selectedItem!.tag {
case 1: model = .oricAtmos
case 2: model = .pravetz
default: break;
default: break
}
return CSStaticAnalyser(oricModel: model, diskInterface: diskInterface)
case "spectrum":
var model: CSMachineSpectrumModel = .plus2a
switch spectrumModelTypeButton.selectedItem!.tag {
case 21: model = .plus2a
case 3: model = .plus3
default: break
}
return CSStaticAnalyser(spectrumModel: model)
case "vic20":
let memorySize = Kilobytes(vic20MemorySizeButton.selectedItem!.tag)
let hasC1540 = vic20HasC1540Button.state == .on

View File

@ -45,6 +45,7 @@ SOURCES += \
$$SRC/Analyser/Static/Oric/*.cpp \
$$SRC/Analyser/Static/Sega/*.cpp \
$$SRC/Analyser/Static/ZX8081/*.cpp \
$$SRC/Analyser/Static/ZXSpectrum/*.cpp \
\
$$SRC/Components/1770/*.cpp \
$$SRC/Components/5380/*.cpp \
@ -88,7 +89,9 @@ SOURCES += \
$$SRC/Machines/MSX/*.cpp \
$$SRC/Machines/Oric/*.cpp \
$$SRC/Machines/Utility/*.cpp \
$$SRC/Machines/ZX8081/*.cpp \
$$SRC/Machines/Sinclair/Keyboard/*.cpp \
$$SRC/Machines/Sinclair/ZX8081/*.cpp \
$$SRC/Machines/Sinclair/ZXSpectrum/*.cpp \
\
$$SRC/Outputs/*.cpp \
$$SRC/Outputs/CRT/*.cpp \
@ -213,7 +216,9 @@ HEADERS += \
$$SRC/Machines/MSX/*.hpp \
$$SRC/Machines/Oric/*.hpp \
$$SRC/Machines/Utility/*.hpp \
$$SRC/Machines/ZX8081/*.hpp \
$$SRC/Machines/Sinclair/Keyboard/*.hpp \
$$SRC/Machines/Sinclair/ZX8081/*.hpp \
$$SRC/Machines/Sinclair/ZXSpectrum/*.hpp \
\
$$SRC/Numeric/*.hpp \
\

View File

@ -1,12 +1,14 @@
#include "mainwindow.h"
#include "settings.h"
#include "timer.h"
#include <QObject>
#include <QStandardPaths>
#include <QtWidgets>
#include <QtGlobal>
#include "mainwindow.h"
#include "settings.h"
#include "timer.h"
#include <cstdio>
#include "../../Numeric/CRC.hpp"
@ -462,6 +464,11 @@ void MainWindow::launchMachine() {
addZX8081Menu(settingsPrefix);
break;
case Analyser::Machine::ZXSpectrum:
addDisplayMenu(settingsPrefix, "Composite", "", "S-Video", "SCART");
addEnhancementsMenu(settingsPrefix, true, false);
break;
default: break;
}
@ -599,7 +606,7 @@ void MainWindow::addZX8081Menu(const std::string &machinePrefix) {
controlsMenu->addAction(startTapeAction);
connect(startTapeAction, &QAction::triggered, this, [=] {
std::lock_guard lock_guard(machineMutex);
static_cast<ZX8081::Machine *>(machine->raw_pointer())->set_tape_is_playing(true);
static_cast<Sinclair::ZX8081::Machine *>(machine->raw_pointer())->set_tape_is_playing(true);
updateTapeControls();
});
@ -607,7 +614,7 @@ void MainWindow::addZX8081Menu(const std::string &machinePrefix) {
controlsMenu->addAction(stopTapeAction);
connect(stopTapeAction, &QAction::triggered, this, [=] {
std::lock_guard lock_guard(machineMutex);
static_cast<ZX8081::Machine *>(machine->raw_pointer())->set_tape_is_playing(false);
static_cast<Sinclair::ZX8081::Machine *>(machine->raw_pointer())->set_tape_is_playing(false);
updateTapeControls();
});
@ -620,7 +627,7 @@ void MainWindow::addZX8081Menu(const std::string &machinePrefix) {
void MainWindow::updateTapeControls() {
const bool startStopEnabled = !automaticTapeControlAction->isChecked();
const bool isPlaying = static_cast<ZX8081::Machine *>(machine->raw_pointer())->get_tape_is_playing();
const bool isPlaying = static_cast<Sinclair::ZX8081::Machine *>(machine->raw_pointer())->get_tape_is_playing();
startTapeAction->setEnabled(!isPlaying && startStopEnabled);
stopTapeAction->setEnabled(isPlaying && startStopEnabled);
@ -1097,6 +1104,7 @@ void MainWindow::setButtonPressed(int index, bool isPressed) {
#include "../../Analyser/Static/MSX/Target.hpp"
#include "../../Analyser/Static/Oric/Target.hpp"
#include "../../Analyser/Static/ZX8081/Target.hpp"
#include "../../Analyser/Static/ZXSpectrum/Target.hpp"
void MainWindow::startMachine() {
const auto selectedTab = ui->machineSelectionTabs->currentWidget();
@ -1115,6 +1123,7 @@ void MainWindow::startMachine() {
TEST(macintosh);
TEST(msx);
TEST(oric);
TEST(spectrum);
TEST(vic20);
TEST(zx80);
TEST(zx81);
@ -1245,6 +1254,18 @@ void MainWindow::start_oric() {
launchTarget(std::move(target));
}
void MainWindow::start_spectrum() {
using Target = Analyser::Static::ZXSpectrum::Target;
auto target = std::make_unique<Target>();
switch(ui->oricModelComboBox->currentIndex()) {
default: target->model = Target::Model::Plus2a; break;
case 1: target->model = Target::Model::Plus3; break;
}
launchTarget(std::move(target));
}
void MainWindow::start_vic20() {
using Target = Analyser::Static::Commodore::Target;
auto target = std::make_unique<Target>();

View File

@ -19,7 +19,7 @@
#include "../../Activity/Observer.hpp"
// There are machine-specific controls for the following:
#include "../../Machines/ZX8081/ZX8081.hpp"
#include "../../Machines/Sinclair/ZX8081/ZX8081.hpp"
#include "../../Machines/Atari/2600/Atari2600.hpp"
QT_BEGIN_NAMESPACE
@ -92,6 +92,7 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat
void start_macintosh();
void start_msx();
void start_oric();
void start_spectrum();
void start_vic20();
void start_zx80();
void start_zx81();

View File

@ -533,6 +533,55 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="spectrumTab">
<attribute name="title">
<string>Spectrum</string>
</attribute>
<layout class="QVBoxLayout" name="spectrumLayout">
<item>
<layout class="QHBoxLayout" name="spectrumHorizontalLayout">
<item>
<layout class="QFormLayout" name="spectrumFormLayout">
<item row="0" column="0">
<widget class="QLabel" name="spectrumModelLabel">
<property name="text">
<string>Model:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="spectrumModelComboBox">
<item>
<property name="text">
<string>+2a</string>
</property>
</item>
<item>
<property name="text">
<string>+3</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="spectrumHSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="vic20Tab">
<attribute name="title">
<string>Vic-20</string>

View File

@ -36,6 +36,7 @@ SOURCES += glob.glob('../../Analyser/Static/MSX/*.cpp')
SOURCES += glob.glob('../../Analyser/Static/Oric/*.cpp')
SOURCES += glob.glob('../../Analyser/Static/Sega/*.cpp')
SOURCES += glob.glob('../../Analyser/Static/ZX8081/*.cpp')
SOURCES += glob.glob('../../Analyser/Static/ZXSpectrum/*.cpp')
SOURCES += glob.glob('../../Components/1770/*.cpp')
SOURCES += glob.glob('../../Components/5380/*.cpp')
@ -82,7 +83,9 @@ SOURCES += glob.glob('../../Machines/MasterSystem/*.cpp')
SOURCES += glob.glob('../../Machines/MSX/*.cpp')
SOURCES += glob.glob('../../Machines/Oric/*.cpp')
SOURCES += glob.glob('../../Machines/Utility/*.cpp')
SOURCES += glob.glob('../../Machines/ZX8081/*.cpp')
SOURCES += glob.glob('../../Machines/Sinclair/Keyboard/*.cpp')
SOURCES += glob.glob('../../Machines/Sinclair/ZX8081/*.cpp')
SOURCES += glob.glob('../../Machines/Sinclair/ZXSpectrum/*.cpp')
SOURCES += glob.glob('../../Outputs/*.cpp')
SOURCES += glob.glob('../../Outputs/CRT/*.cpp')

View File

@ -32,7 +32,7 @@ void CRT::set_new_timing(int cycles_per_line, int height_of_display, Outputs::Di
phase_numerator_ = 0;
colour_cycle_numerator_ = int64_t(colour_cycle_numerator);
phase_alternates_ = should_alternate;
is_alernate_line_ &= phase_alternates_;
should_be_alternate_line_ &= phase_alternates_;
cycles_per_line_ = cycles_per_line;
const int multiplied_cycles_per_line = cycles_per_line * time_multiplier_;
@ -275,7 +275,7 @@ void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_
// If retrace is starting, update phase if required and mark no colour burst spotted yet.
if(next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace) {
is_alernate_line_ ^= phase_alternates_;
should_be_alternate_line_ ^= phase_alternates_;
colour_burst_amplitude_ = 0;
}
}
@ -408,18 +408,19 @@ void CRT::output_level(int number_of_cycles) {
output_scan(&scan);
}
void CRT::output_colour_burst(int number_of_cycles, uint8_t phase, uint8_t amplitude) {
void CRT::output_colour_burst(int number_of_cycles, uint8_t phase, bool is_alternate_line, uint8_t amplitude) {
Scan scan;
scan.type = Scan::Type::ColourBurst;
scan.number_of_cycles = number_of_cycles;
scan.phase = phase;
scan.amplitude = amplitude >> 1;
is_alernate_line_ = is_alternate_line;
output_scan(&scan);
}
void CRT::output_default_colour_burst(int number_of_cycles, uint8_t amplitude) {
// TODO: avoid applying a rounding error here?
output_colour_burst(number_of_cycles, uint8_t((phase_numerator_ * 256) / phase_denominator_), amplitude);
output_colour_burst(number_of_cycles, uint8_t((phase_numerator_ * 256) / phase_denominator_), should_be_alternate_line_, amplitude);
}
void CRT::set_immediate_default_phase(float phase) {

View File

@ -61,7 +61,7 @@ class CRT {
int64_t phase_denominator_ = 1;
int64_t phase_numerator_ = 0;
int64_t colour_cycle_numerator_ = 1;
bool is_alernate_line_ = false, phase_alternates_ = false;
bool is_alernate_line_ = false, phase_alternates_ = false, should_be_alternate_line_ = false;
void advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type, int number_of_samples);
Flywheel::SyncEvent get_next_vertical_sync_event(bool vsync_is_requested, int cycles_to_run_for, int *cycles_advanced);
@ -207,7 +207,7 @@ class CRT {
@param amplitude The amplitude of the colour burst in 1/255ths of the amplitude of the
positive portion of the wave.
*/
void output_colour_burst(int number_of_cycles, uint8_t phase, uint8_t amplitude = DefaultAmplitude);
void output_colour_burst(int number_of_cycles, uint8_t phase, bool is_alternate_line = false, uint8_t amplitude = DefaultAmplitude);
/*! Outputs a colour burst exactly in phase with CRT expectations using the idiomatic amplitude.

Binary file not shown.

View File

@ -0,0 +1,11 @@
Per https://groups.google.com/g/comp.sys.amstrad.8bit/c/HtpBU2Bzv_U/m/HhNDSU3MksAJ :
"Amstrad are happy for emulator writers to include images of our copyrighted
code as long as the (c)opyright messages are not altered and we appreciate
it if the program/manual includes a note to the effect that "Amstrad have
kindly given their permission for the redistribution of their copyrighted
material but retain that copyright"."
With that in mind, Amstrad have kindly given their permission for the redistribution of their copyrighted material but retain that copyright. Material expected here, copyright Amstrad:
plus3.rom — the +2a/+3 ROM file, 64kb in size.

View File

@ -200,12 +200,12 @@ std::size_t CPCDSK::index_for_track(::Storage::Disk::Track::Address address) {
std::shared_ptr<Track> CPCDSK::get_track_at_position(::Storage::Disk::Track::Address address) {
// Given that thesea are interleaved images, determine which track, chronologically, is being requested.
std::size_t chronological_track = index_for_track(address);
const std::size_t chronological_track = index_for_track(address);
// Return a nullptr if out of range or not provided.
if(chronological_track >= tracks_.size()) return nullptr;
Track *track = tracks_[chronological_track].get();
Track *const track = tracks_[chronological_track].get();
if(!track) return nullptr;
std::vector<const Storage::Encodings::MFM::Sector *> sectors;
@ -228,7 +228,7 @@ void CPCDSK::set_tracks(const std::map<::Storage::Disk::Track::Address, std::sha
is_double_density);
// Find slot for track, making it if neccessary.
std::size_t chronological_track = index_for_track(pair.first);
const std::size_t chronological_track = index_for_track(pair.first);
if(chronological_track >= tracks_.size()) {
tracks_.resize(chronological_track+1);
head_position_count_ = pair.first.position.as_int();
@ -310,7 +310,7 @@ void CPCDSK::set_tracks(const std::map<::Storage::Disk::Track::Address, std::sha
// Output each track.
for(std::size_t index = 0; index < size_t(head_position_count_ * head_count_); ++index) {
if(index >= tracks_.size()) continue;
Track *track = tracks_[index].get();
Track *const track = tracks_[index].get();
if(!track) continue;
// Output track header.

View File

@ -0,0 +1,122 @@
//
// SpectrumTAP.cpp
// Clock Signal
//
// Created by Thomas Harte on 19/03/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "ZXSpectrumTAP.hpp"
using namespace Storage::Tape;
/*
The understanding of idiomatic Spectrum data encoding below
is taken from the TZX specifications at
https://worldofspectrum.net/features/TZXformat.html ;
specifics of the TAP encoding were gained from
https://sinclair.wiki.zxnet.co.uk/wiki/TAP_format
*/
ZXSpectrumTAP::ZXSpectrumTAP(const std::string &file_name) :
file_(file_name)
{
// Check for a continuous series of blocks through to
// exactly file end.
//
// To consider: could also check those blocks of type 0
// and type ff for valid checksums?
while(true) {
const uint16_t block_length = file_.get16le();
if(file_.eof()) throw ErrorNotZXSpectrumTAP;
file_.seek(block_length, SEEK_CUR);
if(file_.tell() == file_.stats().st_size) break;
}
virtual_reset();
}
bool ZXSpectrumTAP::is_at_end() {
return file_.tell() == file_.stats().st_size;
}
void ZXSpectrumTAP::virtual_reset() {
file_.seek(0, SEEK_SET);
read_next_block();
}
Tape::Pulse ZXSpectrumTAP::virtual_get_next_pulse() {
// Adopt a general pattern of high then low.
Pulse pulse;
pulse.type = (distance_into_phase_ & 1) ? Pulse::Type::High : Pulse::Type::Low;
switch(phase_) {
default: break;
case Phase::PilotTone: {
// Output: pulses of length 2168;
// 8063 pulses if block type is 0, otherwise 3223;
// then a 667-length pulse followed by a 735-length pulse.
pulse.length = Time(271, 437'500); // i.e. 2168 / 3'500'000
++distance_into_phase_;
// Check whether in the last two.
if(distance_into_phase_ >= (block_type_ ? 8063 : 3223)) {
pulse.length = (distance_into_phase_ & 1) ? Time(667, 3'500'000) : Time(735, 3'500'000);
// Check whether this is the last one.
if(distance_into_phase_ == (block_type_ ? 8064 : 3224)) {
distance_into_phase_ = 0;
phase_ = Phase::Data;
}
}
} break;
case Phase::Data: {
// Output two pulses of length 855 for a 0; two of length 1710 for a 1,
// from MSB to LSB.
pulse.length = (data_byte_ & 0x80) ? Time(1710, 3'500'000) : Time(855, 3'500'000);
++distance_into_phase_;
if(!(distance_into_phase_ & 1)) {
data_byte_ <<= 1;
}
if(!(distance_into_phase_ & 15)) {
if((distance_into_phase_ >> 4) == block_length_) {
if(block_type_) {
distance_into_phase_ = 0;
phase_ = Phase::Gap;
} else {
read_next_block();
}
} else {
data_byte_ = file_.get8();
}
}
} break;
case Phase::Gap:
Pulse gap;
gap.type = Pulse::Type::Zero;
gap.length = Time(1);
read_next_block();
return gap;
}
return pulse;
}
void ZXSpectrumTAP::read_next_block() {
if(is_at_end()) {
phase_ = Phase::Gap;
} else {
block_length_ = file_.get16le();
data_byte_ = block_type_ = file_.get8();
phase_ = Phase::PilotTone;
}
distance_into_phase_ = 0;
}

View File

@ -0,0 +1,62 @@
//
// SpectrumTAP.hpp
// Clock Signal
//
// Created by Thomas Harte on 19/03/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef SpectrumTAP_hpp
#define SpectrumTAP_hpp
#include "../Tape.hpp"
#include "../../FileHolder.hpp"
#include <cstdint>
#include <string>
namespace Storage {
namespace Tape {
/*!
Provides a @c Tape containing an Spectrum-format tape image, which contains a series of
header and data blocks.
*/
class ZXSpectrumTAP: public Tape {
public:
/*!
Constructs a @c ZXSpectrumTAP containing content from the file with name @c file_name.
@throws ErrorNotZXSpectrumTAP if this file could not be opened and recognised as a valid Spectrum-format TAP.
*/
ZXSpectrumTAP(const std::string &file_name);
enum {
ErrorNotZXSpectrumTAP
};
private:
Storage::FileHolder file_;
uint16_t block_length_ = 0;
uint8_t block_type_ = 0;
uint8_t data_byte_ = 0;
enum Phase {
PilotTone,
Data,
Gap
} phase_ = Phase::PilotTone;
int distance_into_phase_ = 0;
void read_next_block();
// Implemented to satisfy @c Tape.
bool is_at_end() override;
void virtual_reset() override;
Pulse virtual_get_next_pulse() override;
};
}
}
#endif /* SpectrumTAP_hpp */

View File

@ -26,7 +26,7 @@ enum Type: IntType {
BBCMaster = 1 << 7,
BBCModelA = 1 << 8,
BBCModelB = 1 << 9,
ColecoVision = 1 << 10,
Coleco = 1 << 10,
Commodore = 1 << 11,
DiskII = 1 << 12,
Sega = 1 << 13,
@ -35,12 +35,13 @@ enum Type: IntType {
Oric = 1 << 16,
ZX80 = 1 << 17,
ZX81 = 1 << 18,
ZXSpectrum = 1 << 19,
Acorn = AcornAtom | AcornElectron | BBCMaster | BBCModelA | BBCModelB,
ZX8081 = ZX80 | ZX81,
AllCartridge = Atari2600 | AcornElectron | ColecoVision | MSX,
AllCartridge = Atari2600 | AcornElectron | Coleco | MSX,
AllDisk = Acorn | AmstradCPC | Commodore | Oric | MSX, // TODO: | AtariST
AllTape = Acorn | AmstradCPC | Commodore | Oric | ZX80 | ZX81 | MSX,
AllTape = Acorn | AmstradCPC | Commodore | Oric | ZX80 | ZX81 | MSX | ZXSpectrum,
};
class TypeDistinguisher {