Merge pull request #1746 from TomHarte/Yeti
MO5: Add provisional floppy drive support.
This commit is contained in:
20 files changed
+398
-49
No files matched your search
@@ -51,6 +51,7 @@
|
||||
#include "Storage/Disk/DiskImage/Formats/G64.hpp"
|
||||
#include "Storage/Disk/DiskImage/Formats/DMK.hpp"
|
||||
#include "Storage/Disk/DiskImage/Formats/FAT12.hpp"
|
||||
#include "Storage/Disk/DiskImage/Formats/FD.hpp"
|
||||
#include "Storage/Disk/DiskImage/Formats/HFE.hpp"
|
||||
#include "Storage/Disk/DiskImage/Formats/IPF.hpp"
|
||||
#include "Storage/Disk/DiskImage/Formats/IMD.hpp"
|
||||
@@ -240,6 +241,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
|
||||
accumulator.try_standard<Disk::DiskImageHolder<Disk::FAT12>>(TargetPlatform::MSX, "dsk");
|
||||
accumulator.try_standard<Disk::DiskImageHolder<Disk::OricMFMDSK>>(TargetPlatform::Oric, "dsk");
|
||||
|
||||
accumulator.try_standard<Disk::DiskImageHolder<Disk::FD>>(TargetPlatform::ThomsonMO, "fd");
|
||||
|
||||
accumulator.try_standard<Disk::DiskImageHolder<Disk::G64>>(TargetPlatform::Commodore8bit, "g64");
|
||||
|
||||
accumulator.try_standard<MassStorage::HDV>(TargetPlatform::AppleII, "hdv");
|
||||
|
||||
@@ -45,6 +45,12 @@ Analyser::Static::TargetList Analyser::Static::Thomson::GetTargets(
|
||||
}
|
||||
}
|
||||
|
||||
// Currently: accept all floppy disks.
|
||||
if(!media.disks.empty()) {
|
||||
target->floppy = MOTarget::Floppy::CD90_640;
|
||||
target->media.disks = media.disks;
|
||||
}
|
||||
|
||||
if(!target->media.empty()) {
|
||||
destination.push_back(std::move(target));
|
||||
}
|
||||
|
||||
@@ -18,12 +18,16 @@ namespace Analyser::Static::Thomson {
|
||||
struct MOTarget: public Analyser::Static::Target, public Reflection::StructImpl<MOTarget> {
|
||||
std::string loading_command;
|
||||
|
||||
ReflectableEnum(Floppy, None, CD90_640);
|
||||
Floppy floppy = Floppy::None;
|
||||
|
||||
MOTarget() : Analyser::Static::Target(Machine::ThomsonMO) {}
|
||||
|
||||
private:
|
||||
friend Reflection::StructImpl<MOTarget>;
|
||||
void declare_fields() {
|
||||
// None yet.
|
||||
AnnounceEnum(Floppy);
|
||||
DeclareField(floppy);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ void WD1770::posit_event(const int new_event_type) {
|
||||
status.data_request = false;
|
||||
status.spin_up = false;
|
||||
});
|
||||
// TODO: potentially should disable motors?
|
||||
} else {
|
||||
if(!(interesting_event_mask_ & int(new_event_type))) return;
|
||||
interesting_event_mask_ &= ~new_event_type;
|
||||
|
||||
@@ -15,7 +15,7 @@ Plus3::Plus3() : WD1770(P1770) {
|
||||
set_control_register(last_control_, 0xff);
|
||||
}
|
||||
|
||||
void Plus3::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
|
||||
void Plus3::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, const size_t drive) {
|
||||
get_drive(drive).set_disk(disk);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const Storage::Disk::Disk *Plus3::disk(const std::string &name) {
|
||||
}
|
||||
|
||||
|
||||
void Plus3::set_control_register(uint8_t control) {
|
||||
void Plus3::set_control_register(const uint8_t control) {
|
||||
// bit 0 => enable or disable drive 1
|
||||
// bit 1 => enable or disable drive 2
|
||||
// bit 2 => side select
|
||||
@@ -43,7 +43,7 @@ void Plus3::set_control_register(uint8_t control) {
|
||||
set_control_register(control, changes);
|
||||
}
|
||||
|
||||
void Plus3::set_control_register(uint8_t control, uint8_t changes) {
|
||||
void Plus3::set_control_register(const uint8_t control, const uint8_t changes) {
|
||||
if(changes&3) {
|
||||
set_drive(control&3);
|
||||
}
|
||||
@@ -57,13 +57,13 @@ void Plus3::set_control_register(uint8_t control, uint8_t changes) {
|
||||
if(changes & 0x08) set_is_double_density(!(control & 0x08));
|
||||
}
|
||||
|
||||
void Plus3::set_motor_on(bool on) {
|
||||
void Plus3::set_motor_on(const bool on) {
|
||||
// TODO: this status should transfer if the selected drive changes. But the same goes for
|
||||
// writing state, so plenty of work to do in general here.
|
||||
get_drive().set_motor_on(on);
|
||||
}
|
||||
|
||||
void Plus3::set_activity_observer(Activity::Observer *observer) {
|
||||
void Plus3::set_activity_observer(Activity::Observer *const observer) {
|
||||
for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) {
|
||||
drive.set_activity_observer(observer, "Drive " + std::to_string(index+1), true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// CD90-640.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/04/2026.
|
||||
// Copyright © 2026 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "CD90-640.hpp"
|
||||
|
||||
using namespace Thomson;
|
||||
|
||||
CD90_640::CD90_640() : WD::WD1770(P1770) {
|
||||
// 325 is a peculiar RPM, but seems to match a spin-up test in the disk ROM that polls the WD1770's status register
|
||||
// index hole bit and counts time. Furthermore there are other machines with unusual RPMs. Could definitely
|
||||
// still just imply an issue elsewhere in the emulator though.
|
||||
emplace_drives(2, 8000000, 325, 2);
|
||||
}
|
||||
|
||||
uint8_t CD90_640::control() {
|
||||
return control_; // Possibly only b7 is loaded?
|
||||
}
|
||||
|
||||
void CD90_640::set_control(const uint8_t value) {
|
||||
control_ = value;
|
||||
|
||||
// Following along from the schematic in https://github.com/OlivierP-To8/CD90-640/ :
|
||||
//
|
||||
// The 74LS123 on the second page is fed with:
|
||||
//
|
||||
// D0 = external data line 7
|
||||
// D1 = line 0
|
||||
// D2 = line 1
|
||||
// D3 = line 2
|
||||
//
|
||||
// It then routes:
|
||||
//
|
||||
// Q0 = DDEN select
|
||||
// Q1 = side sleect
|
||||
// Q2, Q3 = drive selects
|
||||
//
|
||||
set_is_double_density(!(value & 0x80));
|
||||
for_all_drives( [&](Storage::Disk::Drive &drive, size_t) {
|
||||
drive.set_head(value & 1);
|
||||
});
|
||||
set_drive((value >> 1) & 3);
|
||||
}
|
||||
|
||||
void CD90_640::set_motor_on(const bool motor) {
|
||||
for_all_drives( [&](Storage::Disk::Drive &drive, size_t) {
|
||||
drive.set_motor_on(motor);
|
||||
});
|
||||
}
|
||||
|
||||
void CD90_640::set_activity_observer(Activity::Observer *const observer) {
|
||||
for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) {
|
||||
drive.set_activity_observer(observer, "Drive " + std::to_string(index+1), true);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: the code below is fairly boilerplate; can it be factored out?
|
||||
|
||||
void CD90_640::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, const size_t drive) {
|
||||
get_drive(drive).set_disk(disk);
|
||||
}
|
||||
|
||||
const Storage::Disk::Disk *CD90_640::disk(const std::string &name) {
|
||||
const Storage::Disk::Disk *result = nullptr;
|
||||
for_all_drives( [&](Storage::Disk::Drive &drive, size_t) {
|
||||
const auto disk = drive.disk();
|
||||
if(disk && disk->represents(name)) {
|
||||
result = disk;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// CD90-640.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/04/2026.
|
||||
// Copyright © 2026 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Components/1770/1770.hpp"
|
||||
#include "Outputs/Log.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Thomson {
|
||||
|
||||
class CD90_640 final : public WD::WD1770 {
|
||||
public:
|
||||
CD90_640();
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk>, size_t drive);
|
||||
const Storage::Disk::Disk *disk(const std::string &);
|
||||
void set_activity_observer(Activity::Observer *);
|
||||
|
||||
template <int address>
|
||||
uint8_t read() {
|
||||
const uint8_t result = [&] {
|
||||
if(address & 8) {
|
||||
return control();
|
||||
} else {
|
||||
return WD::WD1770::read(address);
|
||||
}
|
||||
} ();
|
||||
Logger::info().append("Read %02x <- %04x", result, address);
|
||||
return result;
|
||||
}
|
||||
|
||||
template <int address>
|
||||
void write(const uint8_t value) {
|
||||
Logger::info().append("Write %02x -> %04x", value, address);
|
||||
if(address & 8) {
|
||||
set_control(value);
|
||||
} else {
|
||||
WD::WD1770::write(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t control();
|
||||
void set_control(uint8_t);
|
||||
uint8_t control_;
|
||||
|
||||
using Logger = Log::Logger<Log::Source::Floppy>;
|
||||
|
||||
void set_motor_on(bool) override;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "MO5.hpp"
|
||||
|
||||
#include "CD90-640.hpp"
|
||||
#include "Video.hpp"
|
||||
|
||||
#include "Activity/Source.hpp"
|
||||
@@ -36,6 +37,7 @@ using Log = Log::Logger<Log::Source::MO5>;
|
||||
static constexpr int ClockRate = 1'000'000;
|
||||
static constexpr uint8_t MusicExpansionMask = 63;
|
||||
|
||||
template <bool has_floppy>
|
||||
struct ConcreteMachine:
|
||||
public Activity::Source,
|
||||
public Configurable::Device,
|
||||
@@ -64,15 +66,24 @@ struct ConcreteMachine:
|
||||
speaker_.set_input_rate(ClockRate);
|
||||
construct_joysticks();
|
||||
|
||||
const auto request = ROM::Request(ROM::Name::ThomasonMO5v11);
|
||||
auto request = ROM::Request(ROM::Name::ThomsonMO5v11);
|
||||
if(has_floppy) {
|
||||
request = request && ROM::Request(ROM::Name::ThomsonCD90_640);
|
||||
}
|
||||
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
const auto &rom = roms.find(ROM::Name::ThomasonMO5v11)->second;
|
||||
const auto &rom = roms.find(ROM::Name::ThomsonMO5v11)->second;
|
||||
std::copy_n(rom.begin(), rom.size(), rom_.begin());
|
||||
|
||||
if(has_floppy) {
|
||||
const auto &floppy_rom = roms.find(ROM::Name::ThomsonCD90_640)->second;
|
||||
std::copy_n(floppy_rom.begin(), floppy_rom.size(), floppy_rom_.begin());
|
||||
}
|
||||
|
||||
Memory::Fuzz(ram_);
|
||||
system_pia_.refresh();
|
||||
|
||||
@@ -112,18 +123,28 @@ struct ConcreteMachine:
|
||||
) {
|
||||
static constexpr auto duration = CPU::M6809::duration<Cycles>(bus_phase);
|
||||
if(video_ += duration) {
|
||||
system_pia_.set<Motorola::MC6821::Control::CB1>(video_.last_valid()->irq());
|
||||
system_pia_.template set<Motorola::MC6821::Control::CB1>(video_.last_valid()->irq());
|
||||
}
|
||||
time_since_audio_update_ += duration;
|
||||
tape_player_.run_for(duration);
|
||||
if(typer_) {
|
||||
typer_->run_for(duration);
|
||||
}
|
||||
if(has_floppy) {
|
||||
fdc_.run_for(duration * 8); // My WD1770 has a nominal clock of 8Mhz.
|
||||
}
|
||||
|
||||
if constexpr (read_write == CPU::M6809::ReadWrite::NoData) {
|
||||
return Cycles(0);
|
||||
} else {
|
||||
if constexpr (read_write != CPU::M6809::ReadWrite::NoData) {
|
||||
if(address >= 0xa7c0 && address < 0xa800) {
|
||||
const auto unmapped = [&] {
|
||||
if constexpr (CPU::M6809::is_read(read_write)) {
|
||||
value = 0xff;
|
||||
Log::info().append("Unhandled read at %04x", +address);
|
||||
} else {
|
||||
Log::info().append("Unhandled write: %02x -> %04x", +value, +address);
|
||||
}
|
||||
};
|
||||
|
||||
switch(address) {
|
||||
case 0xa7c0: access<0xa7c0, read_write>(system_pia_, value); break;
|
||||
case 0xa7c1: access<0xa7c1, read_write>(system_pia_, value); break;
|
||||
@@ -141,19 +162,22 @@ struct ConcreteMachine:
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xa7d0: if(has_floppy) access<0xa7d0, read_write>(fdc_, value); else unmapped(); break;
|
||||
case 0xa7d1: if(has_floppy) access<0xa7d1, read_write>(fdc_, value); else unmapped(); break;
|
||||
case 0xa7d2: if(has_floppy) access<0xa7d2, read_write>(fdc_, value); else unmapped(); break;
|
||||
case 0xa7d3: if(has_floppy) access<0xa7d3, read_write>(fdc_, value); else unmapped(); break;
|
||||
case 0xa7d8: if(has_floppy) access<0xa7d8, read_write>(fdc_, value); else unmapped(); break;
|
||||
|
||||
default:
|
||||
if constexpr (CPU::M6809::is_read(read_write)) {
|
||||
value = 0xff;
|
||||
Log::info().append("Unhandled read at %04x", +address);
|
||||
} else {
|
||||
Log::info().append("Unhandled write: %02x -> %04x", +value, +address);
|
||||
}
|
||||
unmapped();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if constexpr (CPU::M6809::is_read(read_write)) {
|
||||
if(address < 0x2000) {
|
||||
value = start_pointer_[address];
|
||||
} else if(address >= 0xa000 && address < 0xa7c0) {
|
||||
value = has_floppy ? floppy_rom_[address - 0xa000] : 0xff;
|
||||
} else if(address >= 0xc000) {
|
||||
value = rom_[address - 0xc000];
|
||||
|
||||
@@ -174,7 +198,7 @@ struct ConcreteMachine:
|
||||
if(!serialiser) return;
|
||||
|
||||
Storage::Tape::Thomson::MO::Parser parser;
|
||||
const auto dp = m6809_.registers().reg<CPU::M6809::R8::DP>();
|
||||
const auto dp = m6809_.registers().template reg<CPU::M6809::R8::DP>();
|
||||
auto &polarity = ram_[size_t((dp << 8) | 0x44)];
|
||||
auto &data = ram_[size_t((dp << 8) | 0x45)];
|
||||
|
||||
@@ -193,7 +217,7 @@ struct ConcreteMachine:
|
||||
if(!*bit) {
|
||||
polarity ^= 0xff;
|
||||
}
|
||||
m6809_.registers().reg<CPU::M6809::R8::A>() = *bit ? 0xff : 0x00;
|
||||
m6809_.registers().template reg<CPU::M6809::R8::A>() = *bit ? 0xff : 0x00;
|
||||
|
||||
// The parser reads up to the end of the bit. The ROM routine ends about two-thirds
|
||||
// of the way through the bit. So 'rewind' the tape a little.
|
||||
@@ -212,7 +236,7 @@ struct ConcreteMachine:
|
||||
start_pointer_[address] = value;
|
||||
} else {
|
||||
if constexpr (!CPU::M6809::is_read(read_write)) {
|
||||
if(address >= 0xc000) {
|
||||
if(address >= 0xa000) {
|
||||
Log::info().append("ROM write at %04x", +address);
|
||||
}
|
||||
}
|
||||
@@ -234,6 +258,7 @@ private:
|
||||
|
||||
std::array<uint8_t, 0x10000 + 0x2000> ram_;
|
||||
std::array<uint8_t, 0x4000> rom_;
|
||||
std::array<uint8_t, 0x7c0> floppy_rom_;
|
||||
uint8_t *start_pointer_ = nullptr;
|
||||
|
||||
uint8_t *video_page(const bool pixels) {
|
||||
@@ -299,11 +324,11 @@ private:
|
||||
template <Motorola::MC6821::IRQ irq>
|
||||
void set(const bool active) {
|
||||
if constexpr (irq == Motorola::MC6821::IRQ::A) {
|
||||
machine_.m6809_.set<CPU::M6809::Line::FIRQ>(active);
|
||||
machine_.m6809_.template set<CPU::M6809::Line::FIRQ>(active);
|
||||
}
|
||||
|
||||
if constexpr (irq == Motorola::MC6821::IRQ::B) {
|
||||
machine_.m6809_.set<CPU::M6809::Line::IRQ>(active);
|
||||
machine_.m6809_.template set<CPU::M6809::Line::IRQ>(active);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,17 +444,17 @@ private:
|
||||
apply(port_b_, index ? 0x08 : 0x04);
|
||||
|
||||
if(index) {
|
||||
machine_.sound_and_game_pia_.set<Motorola::MC6821::Control::CB1>(is_active);
|
||||
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CB1>(is_active);
|
||||
} else {
|
||||
machine_.sound_and_game_pia_.set<Motorola::MC6821::Control::CA1>(is_active);
|
||||
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CA1>(is_active);
|
||||
}
|
||||
} else {
|
||||
apply(port_b_, index ? 0x80 : 0x40);
|
||||
|
||||
if(index) {
|
||||
machine_.sound_and_game_pia_.set<Motorola::MC6821::Control::CB2>(is_active);
|
||||
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CB2>(is_active);
|
||||
} else {
|
||||
machine_.sound_and_game_pia_.set<Motorola::MC6821::Control::CA2>(is_active);
|
||||
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CA2>(is_active);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -469,9 +494,14 @@ private:
|
||||
Motorola::MC6821::MC6821<SoundAndGamePIAPortHandler, 2, 1> sound_and_game_pia_;
|
||||
|
||||
JustInTimeActor<Video, Cycles> video_;
|
||||
|
||||
// MARK: - Tape and disk.
|
||||
|
||||
Storage::Tape::BinaryTapePlayer tape_player_;
|
||||
bool allow_fast_tape_hack_ = true;
|
||||
|
||||
Thomson::CD90_640 fdc_;
|
||||
|
||||
// MARK: - AudioProducer.
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
@@ -546,7 +576,7 @@ private:
|
||||
}
|
||||
|
||||
HalfCycles get_typer_delay(const std::string &) const final {
|
||||
if(m6809_.get<CPU::M6809::Line::PowerOnReset>()) {
|
||||
if(m6809_.template get<CPU::M6809::Line::PowerOnReset>()) {
|
||||
return Cycles(1'000'000);
|
||||
} else {
|
||||
return Cycles(0);
|
||||
@@ -564,7 +594,16 @@ private:
|
||||
tape_player_.set_tape(media.tapes.front(), TargetPlatform::ThomsonMO);
|
||||
}
|
||||
|
||||
return !media.tapes.empty();
|
||||
if(!has_floppy) {
|
||||
return !media.tapes.empty();
|
||||
}
|
||||
|
||||
size_t index = 0;
|
||||
for(auto &disk: media.disks) {
|
||||
fdc_.set_disk(disk, index);
|
||||
++ index;
|
||||
}
|
||||
return !media.tapes.empty() || !media.disks.empty();
|
||||
}
|
||||
|
||||
ChangeEffect effect_for_file_did_change(const std::string &) const override {
|
||||
@@ -573,8 +612,11 @@ private:
|
||||
|
||||
// MARK: - Activity Source.
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) override {
|
||||
void set_activity_observer(Activity::Observer *const observer) override {
|
||||
tape_player_.set_activity_observer(observer);
|
||||
if(has_floppy) {
|
||||
fdc_.set_activity_observer(observer);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
@@ -611,5 +653,9 @@ std::unique_ptr<Machine> Machine::ThomsonMO(
|
||||
) {
|
||||
using Target = Analyser::Static::Thomson::MOTarget;
|
||||
const Target *const thomson_target = dynamic_cast<const Target *>(target);
|
||||
return std::make_unique<ConcreteMachine>(*thomson_target, rom_fetcher);
|
||||
|
||||
switch(thomson_target->floppy) {
|
||||
case Target::Floppy::None: return std::make_unique<ConcreteMachine<false>>(*thomson_target, rom_fetcher);
|
||||
case Target::Floppy::CD90_640: return std::make_unique<ConcreteMachine<true>>(*thomson_target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
@@ -1021,7 +1021,7 @@ const std::vector<Description> &Description::all_roms() {
|
||||
// Thomson MO and TO machines.
|
||||
//
|
||||
{
|
||||
ThomasonMO5v1,
|
||||
ThomsonMO5v1,
|
||||
"Thomson",
|
||||
"the Thomson MO5 ROM, version 1",
|
||||
"mo5-v1.0.rom",
|
||||
@@ -1029,7 +1029,7 @@ const std::vector<Description> &Description::all_roms() {
|
||||
0xf62d2f3au
|
||||
},
|
||||
{
|
||||
ThomasonMO5v11,
|
||||
ThomsonMO5v11,
|
||||
"Thomson",
|
||||
"the Thomson MO5 ROM, version 1.1",
|
||||
"mo5-v1.1.rom",
|
||||
@@ -1037,6 +1037,15 @@ const std::vector<Description> &Description::all_roms() {
|
||||
0x237c60bfu
|
||||
},
|
||||
|
||||
{
|
||||
ThomsonCD90_640,
|
||||
"Thomson",
|
||||
"the Thomson CD90-640 ROM",
|
||||
"cd90-640.rom",
|
||||
0x7c0,
|
||||
0x5114c0a5u
|
||||
},
|
||||
|
||||
//
|
||||
// Vic-20.
|
||||
//
|
||||
|
||||
@@ -176,8 +176,10 @@ enum Name {
|
||||
SinclairQLJS,
|
||||
|
||||
// Thomson MO and TO machines.
|
||||
ThomasonMO5v1,
|
||||
ThomasonMO5v11,
|
||||
ThomsonMO5v1,
|
||||
ThomsonMO5v11,
|
||||
|
||||
ThomsonCD90_640,
|
||||
|
||||
// Vic-20.
|
||||
Vic20BASIC,
|
||||
|
||||
@@ -133,6 +133,12 @@
|
||||
4B055AEE1FAE9BBF0060FFFF /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B86E2591F8C628F006FAA45 /* Keyboard.cpp */; };
|
||||
4B055AEF1FAE9BF00060FFFF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.cpp */; };
|
||||
4B055AF21FAE9C1C0060FFFF /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B055AF01FAE9C080060FFFF /* OpenGL.framework */; };
|
||||
4B062D1A2F81427D00472717 /* CD90-640.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D192F81427D00472717 /* CD90-640.cpp */; };
|
||||
4B062D1B2F81427D00472717 /* CD90-640.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D192F81427D00472717 /* CD90-640.cpp */; };
|
||||
4B062D1C2F81427D00472717 /* CD90-640.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D192F81427D00472717 /* CD90-640.cpp */; };
|
||||
4B062D1F2F81589C00472717 /* FD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D1E2F81589C00472717 /* FD.cpp */; };
|
||||
4B062D202F81589C00472717 /* FD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D1E2F81589C00472717 /* FD.cpp */; };
|
||||
4B062D212F81589C00472717 /* FD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D1E2F81589C00472717 /* FD.cpp */; };
|
||||
4B06AACB2C645ED00034D014 /* AY38910.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4A762E1DB1A3FA007AAE2E /* AY38910.cpp */; };
|
||||
4B06AACC2C645EDF0034D014 /* Archimedes.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB505842B9634F30031C43C /* Archimedes.cpp */; };
|
||||
4B06AACD2C645EE50034D014 /* Enterprise.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B051CA12676F52200CA44E8 /* Enterprise.cpp */; };
|
||||
@@ -1405,6 +1411,10 @@
|
||||
4B055ABE1FAE98000060FFFF /* MachineForTarget.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MachineForTarget.cpp; sourceTree = "<group>"; };
|
||||
4B055ABF1FAE98000060FFFF /* MachineForTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MachineForTarget.hpp; sourceTree = "<group>"; };
|
||||
4B055AF01FAE9C080060FFFF /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; };
|
||||
4B062D182F81427D00472717 /* CD90-640.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = "CD90-640.hpp"; sourceTree = "<group>"; };
|
||||
4B062D192F81427D00472717 /* CD90-640.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "CD90-640.cpp"; sourceTree = "<group>"; };
|
||||
4B062D1D2F81589C00472717 /* FD.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = FD.hpp; sourceTree = "<group>"; };
|
||||
4B062D1E2F81589C00472717 /* FD.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = FD.cpp; sourceTree = "<group>"; };
|
||||
4B08A2741EE35D56008B7065 /* Z80InterruptTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Z80InterruptTests.swift; sourceTree = "<group>"; };
|
||||
4B08A2761EE39306008B7065 /* TestMachine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestMachine.h; sourceTree = "<group>"; };
|
||||
4B08A2771EE39306008B7065 /* TestMachine.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TestMachine.mm; sourceTree = "<group>"; };
|
||||
@@ -3413,6 +3423,7 @@
|
||||
4B4518911F75FD1B00926311 /* D64.cpp */,
|
||||
4BAF2B4C2004580C00480230 /* DMK.cpp */,
|
||||
4BEBFB4B2002C4BF000708CC /* FAT12.cpp */,
|
||||
4B062D1E2F81589C00472717 /* FD.cpp */,
|
||||
4B4518931F75FD1B00926311 /* G64.cpp */,
|
||||
4B4518951F75FD1B00926311 /* HFE.cpp */,
|
||||
42EB81262B23AAC300429AF4 /* IMD.cpp */,
|
||||
@@ -3420,13 +3431,13 @@
|
||||
4B7773512DE894E400933F03 /* JFD.cpp */,
|
||||
4BB4BFAE22A42F290069048D /* MacintoshIMG.cpp */,
|
||||
4B58601C1F806AB200AEE2E3 /* MFMSectorDump.cpp */,
|
||||
4B5617322F42ADB8003CB7FE /* MOOF.cpp */,
|
||||
4BC131782346DF2B00E4FF3D /* MSA.cpp */,
|
||||
4B0F94FC208C1A1600FE41D9 /* NIB.cpp */,
|
||||
4B4518971F75FD1B00926311 /* OricMFMDSK.cpp */,
|
||||
423820422B1A90BE00964EFE /* PCBooter.cpp */,
|
||||
4B4518991F75FD1B00926311 /* SSD.cpp */,
|
||||
4B7BA03323C58B1E00B98D9E /* STX.cpp */,
|
||||
4B5617322F42ADB8003CB7FE /* MOOF.cpp */,
|
||||
4B6ED2EE208E2F8A0047B343 /* WOZ.cpp */,
|
||||
4B80CD75256CA15E00176FCC /* 2MG.hpp */,
|
||||
4B45188E1F75FD1B00926311 /* AcornADF.hpp */,
|
||||
@@ -3436,6 +3447,7 @@
|
||||
4B4518921F75FD1B00926311 /* D64.hpp */,
|
||||
4BAF2B4D2004580C00480230 /* DMK.hpp */,
|
||||
4BEBFB4C2002C4BF000708CC /* FAT12.hpp */,
|
||||
4B062D1D2F81589C00472717 /* FD.hpp */,
|
||||
4B4518941F75FD1B00926311 /* G64.hpp */,
|
||||
4B4518961F75FD1B00926311 /* HFE.hpp */,
|
||||
42EB81272B23AAC300429AF4 /* IMD.hpp */,
|
||||
@@ -3443,13 +3455,13 @@
|
||||
4B7773502DE894E400933F03 /* JFD.hpp */,
|
||||
4BB4BFAF22A42F290069048D /* MacintoshIMG.hpp */,
|
||||
4B58601D1F806AB200AEE2E3 /* MFMSectorDump.hpp */,
|
||||
4B5617312F42ADB8003CB7FE /* MOOF.hpp */,
|
||||
4BC131792346DF2B00E4FF3D /* MSA.hpp */,
|
||||
4B0F94FD208C1A1600FE41D9 /* NIB.hpp */,
|
||||
4B4518981F75FD1B00926311 /* OricMFMDSK.hpp */,
|
||||
423820432B1A90BE00964EFE /* PCBooter.hpp */,
|
||||
4B45189A1F75FD1B00926311 /* SSD.hpp */,
|
||||
4B7BA03223C58B1E00B98D9E /* STX.hpp */,
|
||||
4B5617312F42ADB8003CB7FE /* MOOF.hpp */,
|
||||
4B6ED2EF208E2F8A0047B343 /* WOZ.hpp */,
|
||||
4BFDD7891F7F2DB4008579B9 /* Utility */,
|
||||
);
|
||||
@@ -5350,8 +5362,10 @@
|
||||
4BCA6FAA2F589C0C000B9537 /* MO5 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4B062D192F81427D00472717 /* CD90-640.cpp */,
|
||||
4BCA6FA92F589C0C000B9537 /* MO5.cpp */,
|
||||
4BBC7DC02F6D94E500E71794 /* Video.cpp */,
|
||||
4B062D182F81427D00472717 /* CD90-640.hpp */,
|
||||
4BBC7DC52F70BAFB00E71794 /* Keyboard.hpp */,
|
||||
4BCA6FA82F589C0C000B9537 /* MO5.hpp */,
|
||||
4BBC7DBF2F6D94E500E71794 /* Video.hpp */,
|
||||
@@ -6538,6 +6552,7 @@
|
||||
4B0ACC03237756F6008902D0 /* Line.cpp in Sources */,
|
||||
4B055AB11FAE86070060FFFF /* Tape.cpp in Sources */,
|
||||
4B96F7CF263E33B10092AEE1 /* DSK.cpp in Sources */,
|
||||
4B062D1C2F81427D00472717 /* CD90-640.cpp in Sources */,
|
||||
4B2B946626377C0200E7097C /* SZX.cpp in Sources */,
|
||||
4BEDA43225B3C700000C2DBD /* Executor.cpp in Sources */,
|
||||
4BC1317B2346DF2B00E4FF3D /* MSA.cpp in Sources */,
|
||||
@@ -6588,6 +6603,7 @@
|
||||
4B055A911FAE85B50060FFFF /* Cartridge.cpp in Sources */,
|
||||
4B8DD39826360DDF00B3C866 /* Z80.cpp in Sources */,
|
||||
4B894525201967B4007DE474 /* Tape.cpp in Sources */,
|
||||
4B062D1F2F81589C00472717 /* FD.cpp in Sources */,
|
||||
4B055AB21FAE860F0060FFFF /* CommodoreTAP.cpp in Sources */,
|
||||
4BC6236F26F426B400F83DFE /* FAT.cpp in Sources */,
|
||||
4B0F1BFD260300D900B85C66 /* ZXSpectrum.cpp in Sources */,
|
||||
@@ -6860,11 +6876,13 @@
|
||||
4B894528201967B4007DE474 /* Disk.cpp in Sources */,
|
||||
4B7773542DE894E400933F03 /* JFD.cpp in Sources */,
|
||||
4B2E86CF25D8D8C70024F1E9 /* Keyboard.cpp in Sources */,
|
||||
4B062D202F81589C00472717 /* FD.cpp in Sources */,
|
||||
4B4C81CA28B56CF800F84AE9 /* MacintoshVolume.cpp in Sources */,
|
||||
4BBB70A4202011C2002FE009 /* MultiMediaTarget.cpp in Sources */,
|
||||
4B7C681627517A59001671EC /* Sprites.cpp in Sources */,
|
||||
4B0ACC02237756ED008902D0 /* Line.cpp in Sources */,
|
||||
4B5617402F44D533003CB7FE /* ROMCatalogue.cpp in Sources */,
|
||||
4B062D1B2F81427D00472717 /* CD90-640.cpp in Sources */,
|
||||
4B89453A201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
|
||||
4BB697CB1D4B6D3E00248BDF /* TimedEventLoop.cpp in Sources */,
|
||||
4BDACBEC22FFA5D20045EF7E /* ncr5380.cpp in Sources */,
|
||||
@@ -7014,10 +7032,12 @@
|
||||
4B778F5923A5F2D00000D260 /* Z80.cpp in Sources */,
|
||||
4BD971392BFC3D9D00C907AA /* ArchimedesStaticAnalyserTests.mm in Sources */,
|
||||
4BBC7DC22F6D94E500E71794 /* Video.cpp in Sources */,
|
||||
4B062D212F81589C00472717 /* FD.cpp in Sources */,
|
||||
4B08A2751EE35D56008B7065 /* Z80InterruptTests.swift in Sources */,
|
||||
4B778F0E23A5EC4F0000D260 /* Tape.cpp in Sources */,
|
||||
4B778F2D23A5EF190000D260 /* MFMDiskController.cpp in Sources */,
|
||||
4B06AAD92C645F5D0034D014 /* SN76489.cpp in Sources */,
|
||||
4B062D1A2F81427D00472717 /* CD90-640.cpp in Sources */,
|
||||
423BDC4A2AB24699008E37B6 /* 8088Tests.mm in Sources */,
|
||||
4B778F2723A5EEF60000D260 /* BinaryDump.cpp in Sources */,
|
||||
4BFCA1241ECBDCB400AC40C1 /* AllRAMProcessor.cpp in Sources */,
|
||||
|
||||
@@ -875,6 +875,28 @@
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>fd</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>floppy525</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Thomson Disk Image</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
|
||||
@@ -3,4 +3,5 @@ ROM files would ordinarily go here; the copyright status of these is uncertain s
|
||||
Expected files:
|
||||
|
||||
mo5-v1.0.rom — the MO5 ROM, version 1;
|
||||
mo5-v1.1.rom — the MO5 ROM, version 1.1.
|
||||
mo5-v1.1.rom — the MO5 ROM, version 1.1;
|
||||
cd90-640.rom — the CD90-640 floppy disk drive ROM.
|
||||
@@ -51,7 +51,7 @@ AcornADF::AcornADF(const std::string &file_name) : MFMSectorDump(file_name) {
|
||||
//
|
||||
// So then .ADF files might be track-interleaved and might not be.
|
||||
|
||||
const auto has_identifier = [&](long location, bool permit_hugo, bool permit_nick) -> bool {
|
||||
const auto has_identifier = [&](const long location, const bool permit_hugo, const bool permit_nick) -> bool {
|
||||
file_.seek(location, Whence::SET);
|
||||
|
||||
uint8_t bytes[4];
|
||||
@@ -119,6 +119,6 @@ int AcornADF::head_count() const {
|
||||
return head_count_;
|
||||
}
|
||||
|
||||
long AcornADF::get_file_offset_for_position(Track::Address address) const {
|
||||
long AcornADF::get_file_offset_for_position(const Track::Address address) const {
|
||||
return (address.position.as_int() * head_count_ + address.head) * (128 << sector_size_) * sectors_per_track_;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// FD.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/04/2026.
|
||||
// Copyright © 2026 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "FD.hpp"
|
||||
|
||||
namespace {
|
||||
// Per https://en.wikipedia.org/wiki/List_of_floppy_disk_formats the Thomson
|
||||
// uses 16 256-byte sectors per track.
|
||||
constexpr int sectors_per_track = 16;
|
||||
constexpr int sector_size = 1;
|
||||
|
||||
constexpr int bytes_per_track = sectors_per_track * (128 << sector_size);
|
||||
}
|
||||
|
||||
using namespace Storage::Disk;
|
||||
|
||||
FD::FD(const std::string &file_name) : MFMSectorDump(file_name) {
|
||||
// Guess disk geometry from file size.
|
||||
switch(file_.stats().st_size) {
|
||||
default: throw Error::InvalidFormat;
|
||||
|
||||
case bytes_per_track * 40:
|
||||
head_count_ = 1;
|
||||
track_count_ = 40;
|
||||
break;
|
||||
|
||||
case bytes_per_track * 80:
|
||||
head_count_ = 1;
|
||||
track_count_ = 80;
|
||||
break;
|
||||
|
||||
case bytes_per_track * 160:
|
||||
head_count_ = 2;
|
||||
track_count_ = 80;
|
||||
break;
|
||||
}
|
||||
|
||||
set_geometry(sectors_per_track, sector_size, 1, Encodings::MFM::Density::Double);
|
||||
}
|
||||
|
||||
HeadPosition FD::maximum_head_position() const {
|
||||
return HeadPosition(track_count_);
|
||||
}
|
||||
|
||||
int FD::head_count() const {
|
||||
return head_count_;
|
||||
}
|
||||
|
||||
long FD::get_file_offset_for_position(const Track::Address address) const {
|
||||
// Determined empirically: disk images are not interleaved.
|
||||
return ((address.head * track_count_) + address.position.as_int()) * bytes_per_track;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// FD.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/04/2026.
|
||||
// Copyright © 2026 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "MFMSectorDump.hpp"
|
||||
|
||||
namespace Storage::Disk {
|
||||
|
||||
/*!
|
||||
Provides a @c Disk containing an FD disk image: a decoded sector dump of a Thomson disk.
|
||||
*/
|
||||
class FD: public MFMSectorDump {
|
||||
public:
|
||||
/*!
|
||||
Construct an @c FD containing content from the file with name @c file_name.
|
||||
|
||||
@throws Storage::FileHolder::Error::CantOpen if this file can't be opened.
|
||||
@throws Error::InvalidFormat if the file doesn't appear to contain a .FD format image.
|
||||
*/
|
||||
FD(const std::string &file_name);
|
||||
|
||||
HeadPosition maximum_head_position() const final;
|
||||
int head_count() const final;
|
||||
|
||||
private:
|
||||
long get_file_offset_for_position(Track::Address) const final;
|
||||
int head_count_;
|
||||
int track_count_;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -14,14 +14,19 @@ using namespace Storage::Disk;
|
||||
|
||||
MFMSectorDump::MFMSectorDump(const std::string &file_name) : file_(file_name) {}
|
||||
|
||||
void MFMSectorDump::set_geometry(int sectors_per_track, uint8_t sector_size, uint8_t first_sector, Encodings::MFM::Density density) {
|
||||
void MFMSectorDump::set_geometry(
|
||||
const int sectors_per_track,
|
||||
const uint8_t sector_size,
|
||||
const uint8_t first_sector,
|
||||
const Encodings::MFM::Density density
|
||||
) {
|
||||
sectors_per_track_ = sectors_per_track;
|
||||
sector_size_ = sector_size;
|
||||
density_ = density;
|
||||
first_sector_ = first_sector;
|
||||
}
|
||||
|
||||
std::unique_ptr<Track> MFMSectorDump::track_at_position(Track::Address address) const {
|
||||
std::unique_ptr<Track> MFMSectorDump::track_at_position(const Track::Address address) const {
|
||||
if(address.head >= head_count()) return nullptr;
|
||||
if(address.position.as_largest() >= maximum_head_position().as_largest()) return nullptr;
|
||||
|
||||
|
||||
@@ -352,8 +352,8 @@ void Drive::process_next_event() {
|
||||
}
|
||||
cycles_since_index_hole_ = 0;
|
||||
|
||||
// Begin a 2ms period of holding the index line pulse active.
|
||||
index_pulse_remaining_ = Cycles((get_input_clock_rate() * 2) / 1000);
|
||||
// Begin a 1ms period of holding the index line pulse active.
|
||||
index_pulse_remaining_ = Cycles((get_input_clock_rate() * 1) / 1000);
|
||||
}
|
||||
if(
|
||||
event_delegate_ &&
|
||||
|
||||
@@ -20,7 +20,7 @@ TapeSerialiser::TapeSerialiser(std::unique_ptr<FormatSerialiser> &&serialiser) :
|
||||
|
||||
std::unique_ptr<TapeSerialiser> Tape::serialiser(const TargetPlatform::Type platform) const {
|
||||
auto serialiser = format_serialiser();
|
||||
if(auto *recipient = dynamic_cast<TargetPlatform::Recipient *>(serialiser.get())) {
|
||||
if(auto recipient = dynamic_cast<TargetPlatform::Recipient *>(serialiser.get())) {
|
||||
recipient->set_target_platforms(platform);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ void TapeSerialiser::reset() {
|
||||
|
||||
Pulse TapeSerialiser::next_pulse() {
|
||||
pulse_ = serialiser_->next_pulse();
|
||||
offset_++;
|
||||
++offset_;
|
||||
return pulse_;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ ClockingHint::Preference TapePlayer::preferred_clocking() const {
|
||||
return (!tape_ || serialiser_->is_at_end()) ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime;
|
||||
}
|
||||
|
||||
void TapePlayer::set_tape(std::shared_ptr<Storage::Tape::Tape> tape, TargetPlatform::Type platform) {
|
||||
void TapePlayer::set_tape(std::shared_ptr<Storage::Tape::Tape> tape, const TargetPlatform::Type platform) {
|
||||
tape_ = tape;
|
||||
serialiser_ = tape->serialiser(platform);
|
||||
|
||||
@@ -106,7 +106,6 @@ bool TapePlayer::has_tape() const {
|
||||
}
|
||||
|
||||
void TapePlayer::next_pulse() {
|
||||
// get the new pulse
|
||||
if(tape_) {
|
||||
current_pulse_ = serialiser_->next_pulse();
|
||||
if(serialiser_->is_at_end()) update_clocking_observer();
|
||||
@@ -171,7 +170,7 @@ void BinaryTapePlayer::set_motor_control(const bool enabled) {
|
||||
void BinaryTapePlayer::update_observer() {
|
||||
const bool lit = motor_is_running_ && !is_at_end();
|
||||
if(observer_ && lit != observer_lit_) {
|
||||
observer_->set_led_status(LEDName, motor_is_running_ && !is_at_end());
|
||||
observer_->set_led_status(LEDName, lit);
|
||||
observer_lit_ = lit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ set(CLK_SOURCES
|
||||
Machines/Sinclair/ZX8081/Video.cpp
|
||||
Machines/Sinclair/ZX8081/ZX8081.cpp
|
||||
Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp
|
||||
Machines/Thomson/MO5/CD90-640.cpp
|
||||
Machines/Thomson/MO5/MO5.cpp
|
||||
Machines/Thomson/MO5/Video.cpp
|
||||
Machines/Utility/MachineForTarget.cpp
|
||||
@@ -206,6 +207,7 @@ set(CLK_SOURCES
|
||||
Storage/Disk/DiskImage/Formats/D64.cpp
|
||||
Storage/Disk/DiskImage/Formats/DMK.cpp
|
||||
Storage/Disk/DiskImage/Formats/FAT12.cpp
|
||||
Storage/Disk/DiskImage/Formats/FD.cpp
|
||||
Storage/Disk/DiskImage/Formats/G64.cpp
|
||||
Storage/Disk/DiskImage/Formats/HFE.cpp
|
||||
Storage/Disk/DiskImage/Formats/IMD.cpp
|
||||
|
||||
Reference in new issue
Block a user