1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-12-01 14:18:29 +00:00

Compare commits

...

89 Commits

Author SHA1 Message Date
Thomas Harte
40820b18ca Adopt new version number. 2025-11-26 18:09:38 -05:00
Thomas Harte
386c438bfd Merge pull request #1649 from TomHarte/EnterprisePages
Enterprise: allow block saves from ROM.
2025-11-26 18:06:40 -05:00
Thomas Harte
94dffe2046 Add virtual destructor to ensure full clean-up. 2025-11-26 17:37:03 -05:00
Thomas Harte
e861a50158 Allow case sensitivity preference to be stated. 2025-11-26 15:35:28 -05:00
Thomas Harte
4bad2aeed5 Avoid bothering a user who is creating a new file. 2025-11-26 14:50:57 -05:00
Thomas Harte
c23791ba1d Comment and collapse a lot of the recent indirectness. 2025-11-26 12:01:22 -05:00
Thomas Harte
f1e4de1670 Standard Xcode churn. 2025-11-26 11:51:24 -05:00
Thomas Harte
203a03ef24 Unify source of memory map layout. 2025-11-26 11:50:45 -05:00
Thomas Harte
edab2a7fd5 Start trying to support host FS user read from ROM. 2025-11-26 09:26:15 -05:00
Thomas Harte
74e6e7a80d Simplify logic, and honour different open modes. 2025-11-26 09:16:06 -05:00
Thomas Harte
b0438c1ce1 Ensure no infinite loop if bookmark data is missing. 2025-11-25 22:06:45 -05:00
Thomas Harte
7db46f76e9 Merge pull request #1648 from TomHarte/MoreOptions
macOS: Add various missing UI options.
2025-11-25 21:34:30 -05:00
Thomas Harte
e98e7b8f91 Add file/directory discriminator. 2025-11-25 21:24:19 -05:00
Thomas Harte
af255a43f3 Mostly introduce exposed path selection for the Enterprise. 2025-11-25 14:02:40 -05:00
Thomas Harte
d86274b039 Expose BeebSID option via UI. 2025-11-25 09:26:52 -05:00
Thomas Harte
d8600500c3 Update version number. 2025-11-23 22:03:27 -05:00
Thomas Harte
d9138016ef Merge pull request #1647 from TomHarte/BeebSIDAnalyser
Attempt BeebSID autodetection.
2025-11-23 21:50:41 -05:00
Thomas Harte
4f6289f293 Merge pull request #1646 from TomHarte/macOSPermissions
Request user permissions for whole-directory access under macOS.
2025-11-23 21:47:21 -05:00
Thomas Harte
8e598882b6 Attempt BeebSID autodetection. 2025-11-23 21:41:55 -05:00
Thomas Harte
2e780e2b11 Don't accept path substitutions. 2025-11-23 21:28:12 -05:00
Thomas Harte
80590d4434 Attempt full permissions loop, including storage. 2025-11-23 21:22:10 -05:00
Thomas Harte
74736d9723 Reconfigure towards permission on demand. 2025-11-23 12:15:08 -05:00
Thomas Harte
5c20fcefc4 Add delegate to validate on a file-by-file basis. 2025-11-23 12:11:47 -05:00
Thomas Harte
e9ba1c4ede Add some text. 2025-11-23 00:33:08 -05:00
Thomas Harte
6767ddde4c Further expand TODOs and notes. 2025-11-23 00:27:23 -05:00
Thomas Harte
56a326d7db Add some attempt to obtain user permissions for folder access. 2025-11-23 00:07:32 -05:00
Thomas Harte
d340a01513 Merge pull request #1643 from TomHarte/EXOSDirect
Enterprise: support .BAS and .COM and corresponding host filing system access.
2025-11-22 12:38:33 -05:00
Thomas Harte
2dd2b279c8 Note reason for that particular way of marking code. 2025-11-22 09:49:06 -05:00
Thomas Harte
88c124aa9f Implement destroy channel, treat it as the fallback for create channel. 2025-11-22 09:46:02 -05:00
Thomas Harte
e2ef3226af Implement destroy channel. 2025-11-22 09:39:05 -05:00
Thomas Harte
507b81a8a4 Implement SetChannelStatus. 2025-11-22 09:16:53 -05:00
Thomas Harte
31d7639761 Force user/native space selection into call sites. 2025-11-21 22:53:58 -05:00
Thomas Harte
10847b3e0b Fill in everything except SetChannelStatus. 2025-11-21 22:44:49 -05:00
Thomas Harte
7b513a95a1 Restructure, separating create from open. 2025-11-21 22:24:31 -05:00
Thomas Harte
411b96128c Further address type conversions. 2025-11-21 22:07:30 -05:00
Thomas Harte
5ef5478861 Resolve signedness warning. 2025-11-21 21:54:43 -05:00
Thomas Harte
50adbaefc8 Support BAS files, use file name from guest. 2025-11-21 21:53:53 -05:00
Thomas Harte
424d57c2c1 Fix executable name. 2025-11-21 21:30:02 -05:00
Thomas Harte
76ed9d1703 Add FileMode to key file loader. 2025-11-21 21:11:27 -05:00
Thomas Harte
c4bab00c6d Set file: as default filing system when in use. 2025-11-21 21:06:00 -05:00
Thomas Harte
ac9fc15981 Banish magic constant. 2025-11-21 17:03:28 -05:00
Thomas Harte
1b977cae64 Add TODO. 2025-11-21 15:56:44 -05:00
Thomas Harte
0cd1921971 Resolve addressing confusion. 2025-11-21 15:54:43 -05:00
Thomas Harte
c193315e17 Further consolidate for style. 2025-11-21 13:28:45 -05:00
Thomas Harte
364996021e Deal correctly with BC = 0. 2025-11-21 13:18:42 -05:00
Thomas Harte
f287c80e39 Pass on EXOS error code if received. 2025-11-21 13:14:52 -05:00
Thomas Harte
f4a9a64c93 Slightly rearrange. 2025-11-21 13:11:05 -05:00
Thomas Harte
c464ffaeac Attempt read block. 2025-11-21 13:08:52 -05:00
Thomas Harte
822aa4155d The read character is supposed to go into B. 2025-11-21 13:02:13 -05:00
Thomas Harte
3d192e22f2 It seems error codes changed at some point. 2025-11-21 12:58:54 -05:00
Thomas Harte
6e8e2b6201 Call EXOS for buffer allocation. 2025-11-21 12:32:06 -05:00
Thomas Harte
16e4144409 Switch to local ROM. 2025-11-20 22:30:15 -05:00
Thomas Harte
6dacc50163 Add dispatch table and hooks. 2025-11-20 21:50:57 -05:00
Thomas Harte
6d71ad9bcc I think this is code, not a pointer. 2025-11-20 20:11:20 -05:00
Thomas Harte
fc5d93f9cc Focus on writing an in-machine ROM. 2025-11-20 20:08:00 -05:00
Thomas Harte
3196840b05 Avoiding losing channel number; implement EOF check. 2025-11-20 17:26:44 -05:00
Thomas Harte
51eea4dea3 Attempt read character. 2025-11-20 17:16:30 -05:00
Thomas Harte
314154e9fd Implement open/create and close, of the key file. 2025-11-20 17:12:31 -05:00
Thomas Harte
4a93264dc5 Add move semantics. 2025-11-20 17:12:15 -05:00
Thomas Harte
39a96a7f73 Proceed to realisation that I'm probably looking in the wrong RAM. 2025-11-20 16:15:04 -05:00
Thomas Harte
9ee7425627 Add missing include for std::shared_ptr. 2025-11-20 15:58:53 -05:00
Thomas Harte
923fdd42ec Add a printf for tracking. 2025-11-20 15:35:06 -05:00
Thomas Harte
50cd28f882 Add three device-descriptor-specific functions. 2025-11-20 15:32:57 -05:00
Thomas Harte
7bc865a2e0 Update CMake sources list. 2025-11-20 13:13:32 -05:00
Thomas Harte
1cf3c77ae9 Forward file bundle and host FS traps to the handler. 2025-11-20 13:11:09 -05:00
Thomas Harte
d3cda5d878 Merge pull request #1644 from TomHarte/Vic20KeyboardPresumption
Vic20 keys: avoid manual range test, remove macro.
2025-11-20 12:54:24 -05:00
Thomas Harte
4f09f38f2e Unify naming, record trap addresses. 2025-11-20 12:49:21 -05:00
Thomas Harte
4980caee1b Merge branch 'T' into EXOSDirect 2025-11-20 12:44:33 -05:00
Thomas Harte
ae89c66b17 Transcribe basic function and error codes. 2025-11-20 12:42:56 -05:00
Thomas Harte
bb7059a9e1 KEEPSAKE. 2025-11-20 12:41:41 -05:00
Thomas Harte
05de67ba76 Avoid manual range test, remove macro. 2025-11-19 22:04:12 -05:00
Thomas Harte
25bf7df4d1 Install FILE IO ROM and list out syscall points. 2025-11-19 21:53:53 -05:00
Thomas Harte
e44cbcc1d5 Add to all project files. 2025-11-19 21:08:46 -05:00
Thomas Harte
c876bcb849 Ensure appropriate-looking .com files get to the Enterprise. 2025-11-19 17:50:16 -05:00
Thomas Harte
febff84421 Add file bundles as an undefined concept. 2025-11-19 17:09:55 -05:00
Thomas Harte
1ca261986e New direction: attempt a first sweep with the EP128Emu ROM. 2025-11-19 17:04:39 -05:00
Thomas Harte
71e319a815 Merge branch 'master' into EXOSDirect 2025-11-19 16:24:35 -05:00
Thomas Harte
a67e222c35 Merge pull request #1642 from TomHarte/WriteableD64
Enable writing to D64 images.
2025-11-19 13:31:34 -05:00
Thomas Harte
e173a93b57 Decode and write sectors. 2025-11-19 13:14:13 -05:00
Thomas Harte
3dbf62ca08 Treat lack of a serialiser as at-end. 2025-11-19 13:13:58 -05:00
Thomas Harte
bb5239e553 Move D64 into a position where it needs to decode sectors. 2025-11-19 12:29:57 -05:00
Thomas Harte
faec5c3f84 Merge pull request #1640 from TomHarte/1540Writing
Give the 1540/1541 the ability to write.
2025-11-18 22:59:23 -05:00
Thomas Harte
9c359627f3 Add optional initial shift delay. 2025-11-18 22:47:19 -05:00
Thomas Harte
2a0208c554 Set up more realistic feedback loop. 2025-11-18 18:26:02 -05:00
Thomas Harte
f513edc006 Attempt full write loop. 2025-11-18 17:03:22 -05:00
Thomas Harte
9e39be282b Start to consolidate relationship around push. 2025-11-18 13:12:41 -05:00
Thomas Harte
d628f75244 Switch to using an assembler. 2025-04-27 14:24:11 -04:00
Thomas Harte
cd09b5d356 Merge branch 'master' into EXOSDirect 2025-04-27 14:23:53 -04:00
Thomas Harte
bb2cf0170d Start trying to draft an EXOS device ROM. 2025-04-24 20:49:54 -04:00
41 changed files with 1951 additions and 242 deletions

View File

@@ -96,6 +96,7 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
auto targetArchimedes = std::make_unique<ArchimedesTarget>();
int bbc_hits = 0;
int electron_hits = 0;
int beebsid_hits = 0;
bool format_prefers_bbc = false;
// Copy appropriate cartridges to the 8-bit targets.
@@ -198,7 +199,12 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
"SRUNLOCK", "SRWIPE", "TUBE", "TYPE", "UNLOCK", "UNPLUG", "UROMS",
"VERIFY", "ZERO"
}) {
if(std::search(file.data.begin(), file.data.end(), command, command+strlen(command)) != file.data.end()) {
if(std::search(
file.data.begin(),
file.data.end(),
command,
command + strlen(command)
) != file.data.end()) {
targetElectron->has_ap6_rom = true;
targetElectron->has_sideways_ram = true;
}
@@ -206,7 +212,6 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
// Look for any 'BBC indicators', i.e. direct access to BBC-specific hardware.
// Also currently a dense search.
const auto hits = [&](const std::initializer_list<uint16_t> collection) {
int hits = 0;
for(const auto address: collection) {
@@ -253,6 +258,18 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
0xfe0c, 0xfe0d, 0xfe0e,
0xfe0f,
});
// While here, look for attempted SID accesses.
beebsid_hits += hits({
// ULA addresses that aren't also the BBC's CRTC.
0xfc20, 0xfc21, 0xfc22, 0xfc23,
0xfc24, 0xfc25, 0xfc26, 0xfc27,
0xfc28, 0xfc29, 0xfc2a, 0xfc2b,
0xfc2c, 0xfc2d, 0xfc2e, 0xfc2f,
0xfc30, 0xfc31, 0xfc32, 0xfc33,
0xfc34, 0xfc35, 0xfc36, 0xfc37,
0xfc38,
});
}
} else if(adfs_catalogue) {
// Archimedes options, implicitly: ADFS, non-Hugo.
@@ -316,6 +333,9 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(
}
}
// Make a BeebSID guess.
targetBBC->has_beebsid = beebsid_hits > 20;
TargetList targets;
if(!targetElectron->media.empty() && !targetBBC->media.empty()) {
if(bbc_hits > electron_hits || (bbc_hits == electron_hits && format_prefers_bbc)) {

View File

@@ -33,7 +33,7 @@ Analyser::Static::TargetList Analyser::Static::Enterprise::GetTargets(
bool
) {
// This analyser can comprehend disks only.
if(media.disks.empty()) return {};
if(media.disks.empty() && media.file_bundles.empty()) return {};
// Otherwise, assume a return will happen.
Analyser::Static::TargetList targets;
@@ -86,7 +86,36 @@ Analyser::Static::TargetList Analyser::Static::Enterprise::GetTargets(
}
}
if(!media.file_bundles.empty()) {
auto &bundle = *media.file_bundles.front();
const auto key = bundle.key_file();
if(key.has_value()) {
auto file = bundle.open(*key, Storage::FileMode::Read);
enum class FileType: uint16_t {
COM = 0x0500,
BAS = 0x0400,
};
// Check for a .COM by inspecting the header.
const auto type = FileType(file.get_le<uint16_t>());
const uint16_t size = file.get_le<uint16_t>();
// There are then 12 bytes of 0 padding that could be tested for.
if((type != FileType::COM && type != FileType::BAS) || size > file.stats().st_size - 16) {
target->media.file_bundles.clear();
} else {
target->loading_command = "run \"file:\"\n";
}
}
// TODO: look for a key file, similar logic to above.
}
if(!target->media.empty()) {
targets.push_back(std::unique_ptr<Analyser::Static::Target>(target));
}
return targets;
}

View File

@@ -63,6 +63,9 @@
#include "Storage/Disk/DiskImage/Formats/STX.hpp"
#include "Storage/Disk/DiskImage/Formats/WOZ.hpp"
// File Bundles.
#include "Storage/FileBundle/FileBundle.hpp"
// Mass Storage Devices (i.e. usually, hard disks)
#include "Storage/MassStorage/Formats/DAT.hpp"
#include "Storage/MassStorage/Formats/DSK.hpp"
@@ -123,6 +126,8 @@ public:
media.cartridges.push_back(instance);
} else if constexpr (std::is_base_of_v<Storage::MassStorage::MassStorageDevice, InstanceT>) {
media.mass_storage_devices.push_back(instance);
} else if constexpr (std::is_base_of_v<Storage::FileBundle::FileBundle, InstanceT>) {
media.file_bundles.push_back(instance);
} else {
static_assert(always_false_v<InstanceT>, "Unexpected type encountered.");
}
@@ -208,13 +213,14 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
accumulator.try_standard<Disk::DiskImageHolder<Disk::AcornADF>>(TargetPlatform::Acorn, "adf");
accumulator.try_standard<Disk::DiskImageHolder<Disk::AmigaADF>>(TargetPlatform::Amiga, "adf");
accumulator.try_standard<Disk::DiskImageHolder<Disk::AcornADF>>(TargetPlatform::Acorn, "adl");
accumulator.try_standard<Disk::DiskImageHolder<Disk::JFD>>(TargetPlatform::Archimedes, "jfd");
accumulator.try_standard<FileBundle::LocalFSFileBundle>(TargetPlatform::Enterprise, "bas");
accumulator.try_standard<Cartridge::BinaryDump>(TargetPlatform::AllCartridge, "bin");
accumulator.try_standard<Tape::CAS>(TargetPlatform::MSX, "cas");
accumulator.try_standard<Tape::TZX>(TargetPlatform::AmstradCPC, "cdt");
accumulator.try_standard<Cartridge::BinaryDump>(TargetPlatform::Coleco, "col");
accumulator.try_standard<FileBundle::LocalFSFileBundle>(TargetPlatform::Enterprise, "com");
accumulator.try_standard<Tape::CSW>(TargetPlatform::AllTape, "csw");
accumulator.try_standard<Disk::DiskImageHolder<Disk::D64>>(TargetPlatform::Commodore8bit, "d64");
@@ -257,6 +263,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
TargetPlatform::Amiga | TargetPlatform::AtariST | TargetPlatform::AmstradCPC | TargetPlatform::ZXSpectrum,
"ipf");
accumulator.try_standard<Disk::DiskImageHolder<Disk::JFD>>(TargetPlatform::Archimedes, "jfd");
accumulator.try_standard<Disk::DiskImageHolder<Disk::MSA>>(TargetPlatform::AtariST, "msa");
accumulator.try_standard<Cartridge::BinaryDump>(TargetPlatform::MSX, "mx2");
accumulator.try_standard<Disk::DiskImageHolder<Disk::NIB>>(TargetPlatform::DiskII, "nib");

View File

@@ -12,6 +12,7 @@
#include "Storage/Cartridge/Cartridge.hpp"
#include "Storage/Disk/Disk.hpp"
#include "Storage/FileBundle/FileBundle.hpp"
#include "Storage/MassStorage/MassStorageDevice.hpp"
#include "Storage/Tape/Tape.hpp"
#include "Storage/TargetPlatforms.hpp"
@@ -33,9 +34,15 @@ struct Media {
std::vector<std::shared_ptr<Storage::Tape::Tape>> tapes;
std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> cartridges;
std::vector<std::shared_ptr<Storage::MassStorage::MassStorageDevice>> mass_storage_devices;
std::vector<std::shared_ptr<Storage::FileBundle::FileBundle>> file_bundles;
bool empty() const {
return disks.empty() && tapes.empty() && cartridges.empty() && mass_storage_devices.empty();
return
disks.empty() &&
tapes.empty() &&
cartridges.empty() &&
mass_storage_devices.empty() &&
file_bundles.empty();
}
Media &operator +=(const Media &rhs) {
@@ -47,6 +54,7 @@ struct Media {
append(tapes, rhs.tapes);
append(cartridges, rhs.cartridges);
append(mass_storage_devices, rhs.mass_storage_devices);
append(file_bundles, rhs.file_bundles);
return *this;
}

View File

@@ -562,7 +562,7 @@ void WD1770::posit_event(const int new_event_type) {
}
set_data_mode(DataMode::Writing);
begin_writing(false);
begin_writing(false, false);
for(int c = 0; c < (get_is_double_density() ? 12 : 6); c++) {
write_byte(0);
}
@@ -755,7 +755,7 @@ void WD1770::posit_event(const int new_event_type) {
}
WAIT_FOR_EVENT(Event1770::IndexHoleTarget);
begin_writing(true);
begin_writing(true, false);
index_hole_count_ = 0;
write_track_write_loop:

View File

@@ -487,7 +487,7 @@ void i8272::posit_event(const int event_type) {
write_data_found_header:
WAIT_FOR_BYTES(get_is_double_density() ? 22 : 11);
begin_writing(true);
begin_writing(true, false);
write_id_data_joiner(command_.command() == Command::WriteDeletedData, true);
@@ -603,7 +603,7 @@ void i8272::posit_event(const int event_type) {
// Wait for the index hole.
WAIT_FOR_EVENT(Event::IndexHole);
index_hole_count_ = 0;
begin_writing(true);
begin_writing(true, false);
// Write start-of-track.
write_start_of_track();

View File

@@ -296,8 +296,9 @@ int DiskII::read_address(int address) {
inputs_ &= ~input_mode;
break;
case 0xf:
if(!(inputs_ & input_mode))
drives_[active_drive_].begin_writing(Storage::Time(1, int(clock_rate_)), false);
if(!(inputs_ & input_mode)) {
drives_[active_drive_].begin_writing(Storage::Time(1, int(clock_rate_)), false, false);
}
inputs_ |= input_mode;
break;
}

View File

@@ -346,7 +346,9 @@ void IWM::select_shift_mode() {
// If writing mode just began, set the drive into write mode and cue up the first output byte.
if(old_shift_mode != ShiftMode::Writing && shift_mode_ == ShiftMode::Writing) {
if(drives_[active_drive_]) drives_[active_drive_]->begin_writing(Storage::Time(1, clock_rate_ / bit_length_.as_integral()), false);
if(drives_[active_drive_]) {
drives_[active_drive_]->begin_writing(Storage::Time(1, clock_rate_ / bit_length_.as_integral()), false, false);
}
shift_register_ = next_output_;
write_handshake_ |= 0x80 | 0x40;
output_bits_remaining_ = 8;

View File

@@ -109,10 +109,7 @@ Cycles MachineBase::perform(const AddressT address, CPU::MOS6502Mk2::data_t<oper
void Machine::run_for(const Cycles cycles) {
m6502_.run_for(cycles);
const bool drive_motor = drive_VIA_port_handler_.motor_enabled();
get_drive().set_motor_on(drive_motor);
if(drive_motor) {
if(get_drive().get_motor_on()) {
Storage::Disk::Controller::run_for(cycles);
}
}
@@ -150,18 +147,40 @@ void MachineBase::process_input_bit(const int value) {
} else {
drive_VIA_port_handler_.set_sync_detected(false);
}
bit_window_offset_++;
++bit_window_offset_;
if(bit_window_offset_ == 8) {
drive_VIA_port_handler_.set_data_input(uint8_t(shift_register_));
bit_window_offset_ = 0;
if(drive_VIA_port_handler_.should_set_overflow()) {
if(set_cpu_overflow_) {
m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(true);
}
} else {
m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(false);
}
else m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(false);
}
// the 1540 does not recognise index holes
void MachineBase::is_writing_final_bit() {
if(set_cpu_overflow_) {
m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(true);
}
}
void MachineBase::process_write_completed() {
m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(false);
serialise_shift_output();
}
void MachineBase::serialise_shift_output() {
auto &drive = get_drive();
uint8_t value = port_a_output_;
for(int c = 0; c < 8; c++) {
drive.write_bit(value & 0x80);
value <<= 1;
}
}
// The 1540 does not recognise index holes.
void MachineBase::process_index_hole() {}
// MARK: - Drive VIA delegate
@@ -174,6 +193,29 @@ void MachineBase::drive_via_did_set_data_density(DriveVIA &, const int density)
set_expected_bit_length(Storage::Encodings::CommodoreGCR::length_of_a_bit_in_time_zone(unsigned(density)));
}
void MachineBase::drive_via_did_set_drive_motor(DriveVIA &, const bool enabled) {
get_drive().set_motor_on(enabled);
}
void MachineBase::drive_via_did_set_write_mode(DriveVIA &, const bool enabled) {
if(enabled) {
begin_writing(false, true);
} else {
end_writing();
}
}
void MachineBase::drive_via_set_to_shifter_output(DriveVIA &, const uint8_t value) {
port_a_output_ = value;
}
void MachineBase::drive_via_should_set_cpu_overflow(DriveVIA &, const bool overflow) {
set_cpu_overflow_ = overflow;
if(!overflow) {
m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(false);
}
}
// MARK: - SerialPortVIA
template <MOS::MOS6522::Port port>
@@ -200,13 +242,17 @@ void SerialPortVIA::set_serial_line_state(
const bool value,
MOS::MOS6522::MOS6522<SerialPortVIA> &via
) {
const auto set = [&](const uint8_t mask) {
port_b_ = (port_b_ & ~mask) | (value ? 0x00 : mask);
};
switch(line) {
default: break;
case ::Commodore::Serial::Line::Data: port_b_ = (port_b_ & ~0x01) | (value ? 0x00 : 0x01); break;
case ::Commodore::Serial::Line::Clock: port_b_ = (port_b_ & ~0x04) | (value ? 0x00 : 0x04); break;
case ::Commodore::Serial::Line::Data: set(0x01); break;
case ::Commodore::Serial::Line::Clock: set(0x04); break;
case ::Commodore::Serial::Line::Attention:
set(0x80);
attention_level_input_ = !value;
port_b_ = (port_b_ & ~0x80) | (value ? 0x00 : 0x80);
via.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(!value);
update_data_line();
break;
@@ -218,7 +264,8 @@ void SerialPortVIA::set_serial_port(Commodore::Serial::Port &port) {
}
void SerialPortVIA::update_data_line() {
// "ATN (Attention) is an input on pin 3 of P2 and P3 that is sensed at PB7 and CA1 of UC3 after being inverted by UA1"
// "ATN (Attention) is an input on pin 3 of P2 and P3 that
// is sensed at PB7 and CA1 of UC3 after being inverted by UA1"
serial_port_->set_output(
::Commodore::Serial::Line::Data,
Serial::LineLevel(!data_level_output_ && (attention_level_input_ != attention_acknowledge_level_))
@@ -249,24 +296,24 @@ void DriveVIA::set_data_input(const uint8_t value) {
port_a_ = value;
}
bool DriveVIA::should_set_overflow() {
return should_set_overflow_;
}
bool DriveVIA::motor_enabled() {
return drive_motor_;
}
template <MOS::MOS6522::Port port, MOS::MOS6522::Line line>
void DriveVIA::set_control_line_output(const bool value) {
if(port == MOS::MOS6522::Port::A && line == MOS::MOS6522::Line::Two) {
should_set_overflow_ = value;
if(set_cpu_overflow_ != value) {
set_cpu_overflow_ = value;
if(delegate_) {
delegate_->drive_via_should_set_cpu_overflow(*this, set_cpu_overflow_);
}
}
}
if(port == MOS::MOS6522::Port::B && line == MOS::MOS6522::Line::Two) {
// TODO: 0 = write, 1 = read.
if(!value) {
printf("NOT IMPLEMENTED: write mode\n");
const bool new_write_mode = !value;
if(new_write_mode != write_mode_) {
write_mode_ = new_write_mode;
if(delegate_) {
delegate_->drive_via_did_set_write_mode(*this, write_mode_);
}
}
}
}
@@ -275,7 +322,13 @@ template <>
void DriveVIA::set_port_output<MOS::MOS6522::Port::B>(const uint8_t value, uint8_t) {
if(previous_port_b_output_ != value) {
// Record drive motor state.
drive_motor_ = value&4;
const bool new_drive_motor = value & 4;
if(new_drive_motor != drive_motor_) {
drive_motor_ = new_drive_motor;
if(delegate_) {
delegate_->drive_via_did_set_drive_motor(*this, drive_motor_);
}
}
// Check for a head step.
const int step_difference = ((value&3) - (previous_port_b_output_&3))&3;
@@ -300,7 +353,9 @@ void DriveVIA::set_port_output<MOS::MOS6522::Port::B>(const uint8_t value, uint8
template <>
void DriveVIA::set_port_output<MOS::MOS6522::Port::A>(const uint8_t value, uint8_t) {
printf("TODO: output is %02x\n", value);
if(delegate_) {
delegate_->drive_via_set_to_shifter_output(*this, value);
}
}
void DriveVIA::set_activity_observer(Activity::Observer *const observer) {

View File

@@ -82,6 +82,10 @@ public:
struct Delegate {
virtual void drive_via_did_step_head(DriveVIA &, int direction) = 0;
virtual void drive_via_did_set_data_density(DriveVIA &, int density) = 0;
virtual void drive_via_did_set_drive_motor(DriveVIA &, bool enabled) = 0;
virtual void drive_via_did_set_write_mode(DriveVIA &, bool write) = 0;
virtual void drive_via_should_set_cpu_overflow(DriveVIA &, bool overflow) = 0;
virtual void drive_via_set_to_shifter_output(DriveVIA &, uint8_t) = 0;
};
void set_delegate(Delegate *);
@@ -91,8 +95,6 @@ public:
void set_sync_detected(bool);
void set_data_input(uint8_t);
void set_is_read_only(bool);
bool should_set_overflow();
bool motor_enabled();
template <MOS::MOS6522::Port, MOS::MOS6522::Line>
void set_control_line_output(bool value);
@@ -104,8 +106,11 @@ public:
private:
uint8_t port_b_ = 0xff, port_a_ = 0xff;
bool should_set_overflow_ = false;
bool set_cpu_overflow_ = false;
bool drive_motor_ = false;
bool write_mode_ = false;
uint8_t previous_port_b_output_ = 0;
Delegate *delegate_ = nullptr;
Activity::Observer *observer_ = nullptr;
@@ -144,8 +149,12 @@ protected:
void mos6522_did_change_interrupt_status(void *mos6522) override;
// to satisfy DriveVIA::Delegate
void drive_via_did_step_head(DriveVIA &, int direction) override;
void drive_via_did_set_data_density(DriveVIA &, int density) override;
void drive_via_did_step_head(DriveVIA &, int) override;
void drive_via_did_set_data_density(DriveVIA &, int) override;
void drive_via_did_set_drive_motor(DriveVIA &, bool) override;
void drive_via_did_set_write_mode(DriveVIA &, bool) override;
void drive_via_should_set_cpu_overflow(DriveVIA &, bool) override;
void drive_via_set_to_shifter_output(DriveVIA &, uint8_t) override;
struct M6502Traits {
static constexpr auto uses_ready_line = false;
@@ -164,9 +173,15 @@ protected:
MOS::MOS6522::MOS6522<DriveVIA> drive_VIA_;
MOS::MOS6522::MOS6522<SerialPortVIA> serial_port_VIA_;
bool set_cpu_overflow_ = false;
int shift_register_ = 0, bit_window_offset_;
void process_input_bit(int value) override;
void process_index_hole() override;
void process_write_completed() override;
void is_writing_final_bit() override;
uint8_t port_a_output_ = 0xff;
void serialise_shift_output();
};
}

View File

@@ -456,27 +456,24 @@ public:
}
void set_key_state(const uint16_t key, const bool is_pressed) final {
if(key < KeyUp) {
const auto apply_shifted = [&](const uint16_t key) {
keyboard_via_port_handler_.set_key_state(KeyLShift, is_pressed);
keyboard_via_port_handler_.set_key_state(key, is_pressed);
} else {
};
switch(key) {
default:
keyboard_via_port_handler_.set_key_state(key, is_pressed);
break;
case KeyRestore:
user_port_via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(!is_pressed);
break;
#define ShiftedMap(source, target) \
case source: \
keyboard_via_port_handler_.set_key_state(KeyLShift, is_pressed); \
keyboard_via_port_handler_.set_key_state(target, is_pressed); \
break;
ShiftedMap(KeyUp, KeyDown);
ShiftedMap(KeyLeft, KeyRight);
ShiftedMap(KeyF2, KeyF1);
ShiftedMap(KeyF4, KeyF3);
ShiftedMap(KeyF6, KeyF5);
ShiftedMap(KeyF8, KeyF7);
#undef ShiftedMap
}
case KeyUp: apply_shifted(KeyDown); break;
case KeyLeft: apply_shifted(KeyRight); break;
case KeyF2: apply_shifted(KeyF1); break;
case KeyF4: apply_shifted(KeyF3); break;
case KeyF6: apply_shifted(KeyF5); break;
case KeyF8: apply_shifted(KeyF7); break;
}
}

View File

@@ -0,0 +1,159 @@
//
// EXOSCodes.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
// Various EXOS codes, transcribed from EXOS20_technical_information.pdf via archive.org,
// which appears to be a compilation of original documentation so page numbers below
// refer to the page within the PDF. Numbers printed on the in-document pages are inconsistent.
namespace Enterprise::EXOS {
// Page 67.
enum class Function: uint8_t {
ResetSystem = 0, // RESET
OpenChannel = 1, // OPEN
CreateChannel = 2, // CREAT
CloseChannel = 3, // CLOSE
DestroyChannel = 4, // DEST
ReadCharacter = 5, // RDCH
ReadBlock = 6, // RDBLK
WriteCharacter = 7, // WRCH
WriteBlock = 8, // WRBLK
ReadChannelStatus = 9, // RSTAT
SetChannelStatus = 10, // SSTAT
SpecialFunction = 11, // SFUNC
SetReadToggleEXOSVariable = 16, // EVAR
CaptureChannel = 17, // CAPT
RedirectChannel = 18, // REDIR
SetDefaultDevice = 19, // DDEV
ReturnSystemStatus = 20, // SYSS
LinkDevices = 21, // LINK
ReadEXOSBoundary = 22, // READB
SetUSERBoundary = 23, // SETB,
AllocateSegment = 24, // ALLOC,
FreeSegment = 25, // FREE
LocateROMs = 26, // ROMS
AllocateChannelBuffer = 27, // BUFF
ReturnErrorMessage = 28, // ERRMSG
};
// Page 25.
enum class DeviceDescriptorFunction: uint8_t {
//
// Codes are the same as `Function` in the range 111.
//
Interrupt = 0,
Initialise = 12,
BufferMoved = 13,
};
enum class Error: uint8_t {
NoError = 0x00,
//
// General Kernel Errors.
//
InvalidFunctionCode = 0xff, // IFUNC
FunctionCallNotAllowed = 0xfe, // ILLFN
InvalidString = 0xfd, // INAME
InsufficientStack = 0xfc, // STACK
ChannelIllegalOrDoesNotExist = 0xfb, // ICHAN
DeviceDoesNotExist = 0xfa, // NODEV
ChannelAlreadyExists = 0xf9, // CHANX
NoAllocateBufferCallMade = 0xf8, // NOBUF
InsufficientRAMForBuffer = 0xf7, // NORAM
InsufficientVideoRAM = 0xf6, // NOVID
NoFreeSegments = 0xf5, // NOSEG
InvalidSegment = 0xf4, // ISEG
InvalidUserBoundary = 0xf3, // IBOUND
InvalidEXOSVariableNumber = 0xf2, // IVAR
InvalidDesviceDescriptorType = 0xf1, // IDESC
UnrecognisedCommandString = 0xf0, // NOSTR
InvalidFileHeader = 0xef, // ASCII
UnknownModuleType = 0xee, // ITYPE
InvalidRelocatableModule = 0xed, // IREL
NoModule = 0xec, // NOMOD
InvalidTimeOrDateValue, // ITIME
//
// General Device Errors.
//
InvalidSpecialFunctionCode = 0xea, // ISPEC
AttemptToOpenSecondChannel = 0xe9, // 2NDCH
InvalidUnitNumber = 0xe8, // IUNIT
FunctionNotSupported = 0xe7, // NOFN
InvalidEscapeSequence = 0xe6, // ESC
StopKeyPressed = 0xe5, // STOP
EndOfFileMetInRead = 0xe4, // EOF
ProtectionViolation = 0xe3, // PROT
//
// Device-Specific Errors.
//
// FileDoesNotExist = 0xea, // NOFIL
// FileAlreadyExists = 0xe9, // EXFIL
// FileAlreadyOpen = 0xe8, // FOPEN
// FileIsTooBig = 0xe6, // FSIZE
// InvalidFilePointerValue = 0xe5, // FPTR
//
// //
// // Keyboard errors.
// //
// InvalidFunctionKeyNumber = 0xe3, // KFKEY
// RunOutOfFunctionKeySpace = 0xe2, // KFSPC
//
// //
// // Sound errors.
// //
// EnvelopeInvalidOrTooBig = 0xe1, // SENV
// NotEnoughRoomToDefineEnvelope = 0xe0, // SENDBF
// EnvelopeStorageRequestedTooSmall = 0xdf, // SENFLO
// SoundQueueFull = 0xde, // SQFUL
//
// //
// // Video errors.
// //
// InvalidRowNumberToScroll = 0xdd, // VROW
// AttemptToMoveCursorOffPage = 0xdc, // VCURS
// InvalidColourPassedToINKOrPAPER = 0xdb, // VCOLR
// InvalidXOrYSizeToOPEN = 0xda, // VSIZE
// InvalidVideoModeToOPEN = 0xd9, // VMODE
// BadParameterToDISPLAY = 0xdb, // VDISP, and officially 'naff' rather than 'bad'
// NotEnoughRowsInPageToDISPLAY = 0xd7, // VDSP2
// AttemptToMoveBeamOffPage = 0xd6, // VBEAM
// LineStyleTooBig = 0xd5, // VLSTY
// LineModeTooBig = 0xd4, // VLMOD
// CantDisplayCharacterOrGraphic = 0xd3, // VCHAR
//
// //
// // Serial errors.
// //
// InvalidBaudRate = 0xd2, // BAUD
//
// //
// // Editor errors.
// //
// InvalidVideoPageForOPEN = 0xd1, // EVID
// TroubleInCommunicatingWithKeyboard = 0xd0, // EKEY
// InvalidCoordinatesForPosition = 0xcf, // ECURS
//
// //
// // Cassette errors.
// //
// CRCErrorFromCassetteDriver = 0xce, // CCRC
//
// //
// // Network errors
// //
// SerialDeviceOpenCannotUseNetwork = 0xcd, // SEROP
// ADDR_NETNotSetUp = 0xcc, // NOADR
};
}

View File

@@ -10,6 +10,7 @@
#include "Dave.hpp"
#include "EXDos.hpp"
#include "HostFSHandler.hpp"
#include "Keyboard.hpp"
#include "Nick.hpp"
@@ -23,6 +24,8 @@
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "Processors/Z80/Z80.hpp"
#include <unordered_set>
namespace {
using Logger = Log::Logger<Log::Source::Enterprise>;
}
@@ -72,6 +75,7 @@ template <bool has_disk_controller, bool is_6mhz> class ConcreteMachine:
public Activity::Source,
public Configurable::Device,
public CPU::Z80::BusHandler,
public HostFSHandler::MemoryAccessor,
public Machine,
public MachineTypes::AudioProducer,
public MachineTypes::MappedKeyboardMachine,
@@ -104,7 +108,8 @@ public:
z80_(*this),
nick_(ram_.end() - 65536),
dave_audio_(audio_queue_),
speaker_(dave_audio_) {
speaker_(dave_audio_),
host_fs_(*this) {
// Request a clock of 4Mhz; this'll be mapped upwards for Nick and downwards for Dave elsewhere.
set_clock_rate(clock_rate);
@@ -228,6 +233,14 @@ public:
memcpy(exdos_rom_.data(), exdos->second.data(), std::min(exdos_rom_.size(), exdos->second.size()));
}
// Possibly install the host FS ROM.
host_fs_rom_.fill(0xff);
if(!target.media.file_bundles.empty()) {
const auto rom = host_fs_.rom();
std::copy(rom.begin(), rom.end(), host_fs_rom_.begin());
find_host_fs_hooks();
}
// Seed key state.
clear_all_keys();
@@ -539,8 +552,40 @@ public:
}
break;
case PartialMachineCycle::Read:
case PartialMachineCycle::ReadOpcode:
{
static bool print_opcode = false;
if(print_opcode) {
printf("%04x: %02x\n", address, read_pointers_[address >> 14][address]);
}
}
// Potential segue for the host FS. I'm relying on branch prediction to
// avoid this cost almost always.
if(test_host_fs_traps_ && (address >> 14) == 3) [[unlikely]] {
const auto is_trap = host_fs_traps_.contains(address);
if(is_trap) {
using Register = CPU::Z80::Register;
uint8_t a = uint8_t(z80_.value_of(Register::A));
uint16_t bc = z80_.value_of(Register::BC);
uint16_t de = z80_.value_of(Register::DE);
// Grab function code from where the PC actually is, and return a NOP
host_fs_.perform(read_pointers_[address >> 14][address], a, bc, de);
*cycle.value = 0x00; // i.e. NOP.
z80_.set_value_of(Register::A, a);
z80_.set_value_of(Register::BC, bc);
z80_.set_value_of(Register::DE, de);
break;
}
}
[[fallthrough]];
case PartialMachineCycle::Read:
if(read_pointers_[address >> 14]) {
*cycle.value = read_pointers_[address >> 14][address];
} else {
@@ -570,58 +615,104 @@ public:
private:
// MARK: - Memory layout
std::array<uint8_t, 256 * 1024> ram_{};
std::array<uint8_t, 64 * 1024> exos_;
std::array<uint8_t, 16 * 1024> basic_;
std::array<uint8_t, 16 * 1024> exdos_rom_;
std::array<uint8_t, 32 * 1024> epdos_rom_;
std::array<uint8_t, 16 * 1024> host_fs_rom_;
const uint8_t min_ram_slot_;
/// @returns A pointer to the start of the RAM segment representing @c page if any; otherwise @c nullptr.
uint8_t *ram_segment(const uint8_t page) {
if(page < min_ram_slot_) return nullptr;
const auto ram_floor = (0x100 << 14) - ram_.size();
// Each segment is 2^14 bytes long and there are 256 of them. So the Enterprise has a 22-bit address space.
// RAM is at the end of that range; `ram_floor` is the 22-bit address at which RAM starts.
return &ram_[size_t((page << 14)) - ram_floor];
}
struct ROMPage {
const uint8_t *rom;
uint8_t page_offset;
operator bool() const {
return bool(rom);
}
uint8_t operator[](const size_t offset) const {
return rom[page_offset * 0x4000 + offset];
}
};
/// @returns A pointer to the ROM segment representing @c page if any; otherwise an object that converts to bool @c false. The returned object
/// names both the start of the ROM and how many pages into it this page rests.
ROMPage rom_segment(const uint8_t page) {
const auto rom_segment = [&](const uint8_t base, auto &source) -> ROMPage {
if(page < base || page >= base + source.size() / 0x4000) {
return { nullptr, 0 };
}
return {
source.data(),
uint8_t(page - base)
};
};
// This is where I've effectively dictated the overall 22-bit RAM layout.
// The first argument before each ROM dictates its starting page.
if(const auto segment = rom_segment(0, exos_); segment.rom) return segment;
if(const auto segment = rom_segment(16, basic_); segment.rom) return segment;
if(const auto segment = rom_segment(32, exdos_rom_); segment.rom) return segment;
if(const auto segment = rom_segment(48, epdos_rom_); segment.rom) return segment;
if(const auto segment = rom_segment(64, host_fs_rom_); segment.rom) return segment;
return { nullptr, 0 };
}
// Ephemeral, user-set state, representing the current memory map as viewed from the Z80.
const uint8_t *read_pointers_[4] = {nullptr, nullptr, nullptr, nullptr};
uint8_t *write_pointers_[4] = {nullptr, nullptr, nullptr, nullptr};
uint8_t pages_[4] = {0x80, 0x80, 0x80, 0x80};
template <size_t slot, typename RomT>
bool page_rom(const uint8_t offset, const uint8_t location, const RomT &source) {
if(offset < location || offset >= location + source.size() / 0x4000) {
return false;
}
page<slot>(&source[(offset - location) * 0x4000], nullptr);
is_video_[slot] = false;
return true;
}
/// Pages whatever is supposed to be at @c offset into memory at @c slot, whether ROM or RAM, and updates the
/// @c test_host_fs_traps_ and relevant @c is_video_ flags.
template <size_t slot> void page(const uint8_t offset) {
const auto apply = [&](const uint8_t *const read, uint8_t *const write) {
read_pointers_[slot] = read ? read - (slot * 0x4000) : nullptr;
write_pointers_[slot] = write ? write - (slot * 0x4000) : nullptr;
};
pages_[slot] = offset;
if(page_rom<slot>(offset, 0, exos_)) return;
if(page_rom<slot>(offset, 16, basic_)) return;
if(page_rom<slot>(offset, 32, exdos_rom_)) return;
if(page_rom<slot>(offset, 48, epdos_rom_)) return;
const auto rom = rom_segment(offset);
if constexpr (slot == 3) {
if(rom) {
test_host_fs_traps_ = rom.rom == host_fs_rom_.data();
} else {
test_host_fs_traps_ = false;
}
}
if(rom) {
apply(rom.rom + rom.page_offset * 0x4000, nullptr);
is_video_[slot] = false;
return;
}
// Of whatever size of RAM I've declared above, use only the final portion.
// This correlated with Nick always having been handed the final 64kb and,
// at least while the RAM is the first thing declared above, does a little
// to benefit data locality. Albeit not in a useful sense.
if(offset >= min_ram_slot_) {
const auto ram_floor = 4194304 - ram_.size();
const size_t address = offset * 0x4000 - ram_floor;
is_video_[slot] = offset >= 0xfc; // TODO: this hard-codes a 64kb video assumption.
page<slot>(&ram_[address], &ram_[address]);
auto pointer = ram_segment(offset);
apply(pointer, pointer);
return;
}
page<slot>(nullptr, nullptr);
}
template <size_t slot> void page(const uint8_t *const read, uint8_t *const write) {
read_pointers_[slot] = read ? read - (slot * 0x4000) : nullptr;
write_pointers_[slot] = write ? write - (slot * 0x4000) : nullptr;
apply(nullptr, nullptr);
}
// MARK: - Memory Timing
// The wait mode affects all memory accesses _outside of the video area_.
enum class WaitMode {
None,
@@ -631,6 +722,7 @@ private:
bool is_video_[4]{};
// MARK: - ScanProducer
void set_scan_target(Outputs::Display::ScanTarget *const scan_target) override {
nick_.last_valid()->set_scan_target(scan_target);
}
@@ -706,11 +798,14 @@ private:
}
}
if(!media.file_bundles.empty()) {
host_fs_.set_file_bundle(media.file_bundles.front());
}
return true;
}
// MARK: - Interrupts
uint8_t interrupt_mask_ = 0x00, interrupt_state_ = 0x00;
void set_interrupts(const uint8_t mask, const HalfCycles offset = HalfCycles(0)) {
interrupt_state_ |= uint8_t(mask);
@@ -742,9 +837,93 @@ private:
}
// MARK: - EXDos card.
EXDos exdos_;
// MARK: - Host FS.
HostFSHandler host_fs_;
std::unordered_set<uint16_t> host_fs_traps_;
bool test_host_fs_traps_ = false;
/// Reads from mamory as currently laid out.
uint8_t hostfs_read(const uint16_t address) override {
if(read_pointers_[address >> 14]) {
return read_pointers_[address >> 14][address];
} else {
return 0xff;
}
}
/// @returns The page that should be used to access memory at @c address within the current user memory map.
/// This is purely an EXOS construct. It has no basis in hardware.
uint8_t user_page(const uint16_t address) {
const auto page_id = address >> 14;
return read_pointers_[0xbffc >> 14] ? read_pointers_[0xbffc >> 14][0xbffc + page_id] : 0xff;
}
/// @returns The byte of RAM at @c address in the user memory map, if RAM is paged there. @c nullptr otherwise.
uint8_t *user_ram(const uint8_t page, const uint16_t address) {
// "User" accesses go to to wherever the user last had paged;
// per 5.4 System Segment Usage those pages are stored in memory from
// 0xbffc, so grab from there.
const auto offset = address & 0x3fff;
auto segment = ram_segment(page);
if(segment) {
return &segment[offset];
}
return nullptr;
}
/// @returns The byte at @c address in the user memory map, whether ROM or RAM.
uint8_t hostfs_user_read(const uint16_t address) override {
const auto page = user_page(address);
const auto ram = ram_segment(page);
if(ram) return ram[address & 0x3fff];
const auto rom = rom_segment(page);
if(rom) return rom[address & 0x3fff];
return 0xff;
}
/// Writes a byte to an address in the user memory map, if it's RAM. Otherwise acts as a no-op.
void hostfs_user_write(const uint16_t address, const uint8_t value) override {
const auto ram = user_ram(user_page(address), address);
if(ram) *ram = value;
}
/// Searches @c host_fs_rom_ for high-level hooks and records those addresses into @c host_fs_traps_ with the assumption that the rom will
/// be paged at 0xc000. Then covers up the hook with NOPs other than the first byte, which captures the hook code.
void find_host_fs_hooks() {
static constexpr uint8_t syscall[] = {
0xed, 0xfe, 0xfe
};
auto begin = host_fs_rom_.begin();
while(true) {
begin = std::search(
begin, host_fs_rom_.end(),
std::begin(syscall), std::end(syscall)
);
if(begin == host_fs_rom_.end()) {
break;
}
const auto offset = begin - host_fs_rom_.begin() + 0xc000; // ROM will be paged in slot 3, i.e. at $c000.
host_fs_traps_.insert(uint16_t(offset));
// Move function code up to where this trap was, and NOP out the tail.
begin[0] = begin[3];
begin[1] = begin[2] = begin[3] = 0x00;
begin += 4;
}
}
// MARK: - Activity Source
void set_activity_observer([[maybe_unused]] Activity::Observer *const observer) final {
if constexpr (has_disk_controller) {
exdos_.set_activity_observer(observer);
@@ -752,6 +931,7 @@ private:
}
// MARK: - Configuration options.
std::unique_ptr<Reflection::Struct> get_options() const final {
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
options->output = get_video_signal_configurable();

View File

@@ -0,0 +1 @@
pyz80.py --obj=hostfs.rom hostfs.z80s && xxd -i hostfs.rom

View File

@@ -0,0 +1,215 @@
;
; Designed for assembly with pyz80, https://github.com/simonowen/pyz80/
; E.g. pyz80 --obj=hostfs.rom hostfs.z80s
;
;
; Sources:
;
; http://ep.homeserver.hu/Dokumentacio/Konyvek/EXOS_2.1_technikal_information/exos/kernel/Ch9.html
; on signature, device chain pointer and ROM entry point
;
; http://ep.homeserver.hu/Dokumentacio/Konyvek/EXOS_2.1_technikal_information/exos/kernel/Ch6.html
; on the device chain
;
;
; This code adapts the same mechanism for a host call as that used by EP128Emu's FILE IO ROM.
; My original thinking was that one could be substituted for the other to permit comparative testing.
; EP128 has a couple of emulator-specific call codes that I don't implement though, and otherwise
; doesn't seem to work in this emulator. And likely the converse holds.
;
hostfscall: macro
db 0xed, 0xfe, 0xfe
db \0
endm
exoscall: macro
rst 0x30
db \0
endm
org 0xc000
dm "EXOS_ROM" ; Standard ROM signature.
; Pointer to the included device chain, which should be valid when this
; ROM is paged at $4000, though when executed from it'll be at $c000.
dw 0x4000 + (device_chain & 0x3fff)
; ROM entry point; handle nothing.
ret
dw 0 ; XX_NEXT_LOW/HI: Pointer to start of next device. There is no next device.
dw 0xfffe ; XX_RAM_LOW/HI: [(Amount of host RAM used) + 2] negatived.
device_chain_type:
db 0 ; DD_TYPE: Type, which must be 0.
db 0 ; DD_IRQFLAG: No interrupts required.
db 0 ; DD_FLAGS: Not a video device.
dw 0x4000 + (dispatch & 0x3fff)
db 0 ; DD_TAB_LOW/HI/SEG:
db 0 ; DD_UNIT_COUNT: ?
device_name:
db 4
dm "FILE" ; DD_NAME
device_chain:
dw device_chain - device_chain_type
dispatch:
@dispatch: EQU FOR 14
dw call{@dispatch}
NEXT @dispatch
;
; Interrupt.
;
; The device chain indicates that this ROM doesn't receive interrupts. So no need to escalate.
;
call0:
ret
;
; Open channel.
;
; EXOS requires the programmer manually to call its function 27 to allocate a channel buffer if
; it otherwise expects to succeed. So some handling is most easily done within the client machine.
;
call1:
ld b, a ; Backup the channel number
hostfscall 1
call allocate_exos_buffer
ret z ; Exit on success.
; Otherwise, close the file and return the EXOS error.
ld c, a
ld a, b
hostfscall 3
ld a, c
ret
;
; Create channel.
;
call2:
ld b, a
hostfscall 2
call allocate_exos_buffer
ret z ; Exit on success.
; Otherwise, erase the newly-created file and return the EXOS error.
ld c, a
ld a, b
hostfscall 4
ld a, c
ret
;
; Close channel.
;
call3:
hostfscall 3
ret
;
; Destroy channel.
;
call4:
hostfscall 4
ret
;
; Read character.
;
call5:
hostfscall 5
ret
;
; Read block.
;
call6:
hostfscall 6
ret
;
; Write character.
;
call7:
hostfscall 7
ret
;
; Write block.
;
call8:
hostfscall 8
ret
;
; Read channel status.
;
call9:
hostfscall 9
ret
;
; Set channel status.
;
call10:
hostfscall 10
ret
;
; Special function.
;
call11:
hostfscall 11
ret
;
; Initialise.
;
call12:
hostfscall 12
;
; Set this as the default filing system.
; Disk dives do this, it's not unprecedented.
;
ld de, device_name
ld c, 1
exoscall 19
ret
;
; Buffer moved.
;
call13:
hostfscall 13
ret
;
; Attempts to allocate EXOS storage for a channel.
; Returns Z set for success, clear for failure.
;
allocate_exos_buffer:
; Exit immediately if that call already failed.
and a
ret nz
; Restore the channel number and otherwise configure to allocate a buffer.
push bc
ld a, b
ld bc, 0
ld de, 1
exoscall 27
; If there's no error from that, exit.
pop bc
and a
ret

View File

@@ -0,0 +1,263 @@
//
// HostFSHandler.cpp
// Clock Signal
//
// Created by Thomas Harte on 20/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#include "HostFSHandler.hpp"
#include "EXOSCodes.hpp"
using namespace Enterprise;
HostFSHandler::HostFSHandler(MemoryAccessor &accessor) : accessor_(accessor) {}
void HostFSHandler::perform(const uint8_t function, uint8_t &a, uint16_t &bc, uint16_t &de) {
const auto set_error = [&](const EXOS::Error error) {
a = uint8_t(error);
};
const auto set_b = [&](const uint8_t ch) {
bc = uint16_t((bc & 0xffff) | (ch << 8));
};
const auto set_c = [&](const uint8_t ch) {
bc = (bc & 0xff00) | (ch);
};
const auto b = [&]() -> uint8_t {
return bc >> 8;
};
const auto read_name = [&]() {
// Get name.
uint8_t length = accessor_.hostfs_read(de++);
std::string name;
while(length--) {
name.push_back(char(accessor_.hostfs_read(de++)));
}
// Use the key file if no name is specified.
if(name.empty()) {
if(const auto key_file = bundle_->key_file(); key_file.has_value()) {
name = *key_file;
}
}
return name;
};
//
// Functions that don't require an existing channel.
//
switch(function) {
default: break;
case uint8_t(EXOS::DeviceDescriptorFunction::Initialise):
channels_.clear();
set_error(EXOS::Error::NoError);
return;
case uint8_t(EXOS::DeviceDescriptorFunction::Interrupt):
case uint8_t(EXOS::DeviceDescriptorFunction::BufferMoved):
set_error(EXOS::Error::NoError);
return;
// Page 54.
// Emprically: C contains the unit number.
case uint8_t(EXOS::Function::OpenChannel): {
if(a == 255) {
set_error(EXOS::Error::ChannelIllegalOrDoesNotExist);
break;
}
const auto name = read_name();
try {
channels_.emplace(a, bundle_->open(name, Storage::FileMode::ReadWrite));
set_error(EXOS::Error::NoError);
} catch(Storage::FileHolder::Error) {
try {
channels_.emplace(a, bundle_->open(name, Storage::FileMode::Read));
set_error(EXOS::Error::NoError);
} catch(Storage::FileHolder::Error) {
// set_error(EXOS::Error::FileDoesNotExist);
set_error(EXOS::Error::ProtectionViolation);
}
}
}
return;
// Page 54.
case uint8_t(EXOS::Function::CreateChannel): {
if(a == 255) {
set_error(EXOS::Error::ChannelIllegalOrDoesNotExist);
break;
}
const auto name = read_name();
try {
channels_.emplace(a, bundle_->open(name, Storage::FileMode::Rewrite));
set_error(EXOS::Error::NoError);
} catch(Storage::FileHolder::Error) {
// set_error(EXOS::Error::FileAlreadyExists);
set_error(EXOS::Error::ProtectionViolation);
}
} return;
case uint8_t(EXOS::Function::SpecialFunction):
// Not supported;
set_error(EXOS::Error::InvalidSpecialFunctionCode);
return;
}
//
// Functions from here require a channel already open.
//
const auto channel = channels_.find(a);
if(channel == channels_.end()) {
set_error(EXOS::Error::ChannelIllegalOrDoesNotExist);
return;
}
auto &file = channel->second;
switch(function) {
default:
printf("UNIMPLEMENTED function %d with A:%02x BC:%04x DE:%04x\n", function, a, bc, de);
break;
// Page 54.
case uint8_t(EXOS::Function::CloseChannel):
set_error(EXOS::Error::NoError);
channels_.erase(channel);
break;
// Page 54.
case uint8_t(EXOS::Function::DestroyChannel): {
const auto name = file.name();
channels_.erase(channel);
if(bundle_->erase(name)) {
set_error(EXOS::Error::NoError);
} else {
set_error(EXOS::Error::ProtectionViolation);
}
} break;
// Page 55.
case uint8_t(EXOS::Function::ReadCharacter): {
const auto next = file.get();
if(file.eof()) {
set_error(EXOS::Error::EndOfFileMetInRead);
} else {
set_b(next);
set_error(EXOS::Error::NoError);
}
} break;
// Page 55.
case uint8_t(EXOS::Function::WriteCharacter): {
if(file.put(b())) {
set_error(EXOS::Error::NoError);
} else {
set_error(EXOS::Error::EndOfFileMetInRead);
}
} break;
// Page 55.
case uint8_t(EXOS::Function::ReadBlock): {
set_error(EXOS::Error::NoError);
while(bc) {
const auto next = file.get();
if(channel->second.eof()) {
set_error(EXOS::Error::EndOfFileMetInRead);
break;
}
accessor_.hostfs_user_write(de++, next);
--bc;
}
} break;
// Page 56.
case uint8_t(EXOS::Function::WriteBlock): {
set_error(EXOS::Error::NoError);
while(bc) {
const auto next = accessor_.hostfs_user_read(de);
if(!file.put(next)) {
set_error(EXOS::Error::EndOfFileMetInRead);
break;
}
++de;
--bc;
}
} break;
// Page 56.
case uint8_t(EXOS::Function::ReadChannelStatus):
a = file.eof() ? 0xff : 0x00;
break;
// Page 56.
case uint8_t(EXOS::Function::SetChannelStatus): {
if(bc & 4) {
// Protection byte is not supported.
set_error(EXOS::Error::FunctionNotSupported);
break;
}
if(bc & 1) { // User is requesting a seek.
auto pointer = de;
uint32_t file_pointer;
file_pointer = accessor_.hostfs_user_read(pointer++);
file_pointer |= uint32_t(accessor_.hostfs_user_read(pointer++) << 8);
file_pointer |= uint32_t(accessor_.hostfs_user_read(pointer++) << 16);
file_pointer |= uint32_t(accessor_.hostfs_user_read(pointer++) << 24);
if(!file.seek(file_pointer, Storage::Whence::SET)) {
set_error(EXOS::Error::EndOfFileMetInRead);
break;
}
}
// Fill in both position and length.
set_c(3);
const uint32_t file_pointer = uint32_t(file.tell());
const uint32_t file_length = uint32_t(file.stats().st_size);
auto pointer = de;
const auto write = [&](const uint32_t source) {
accessor_.hostfs_user_write(pointer++, uint8_t(source >> 0));
accessor_.hostfs_user_write(pointer++, uint8_t(source >> 8));
accessor_.hostfs_user_write(pointer++, uint8_t(source >> 16));
accessor_.hostfs_user_write(pointer++, uint8_t(source >> 24));
};
write(file_pointer);
write(file_length);
set_error(EXOS::Error::NoError);
} break;
}
}
void HostFSHandler::set_file_bundle(std::shared_ptr<Storage::FileBundle::FileBundle> bundle) {
bundle_ = bundle;
bundle_->set_case_insensitive(true);
}
std::vector<uint8_t> HostFSHandler::rom() {
// Assembled and transcribed from hostfs.z80.
return std::vector<uint8_t>{
0x45, 0x58, 0x4f, 0x53, 0x5f, 0x52, 0x4f, 0x4d, 0x1b, 0x40, 0xc9, 0x00,
0x00, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x1d, 0x40, 0x00, 0x00, 0x04, 0x46,
0x49, 0x4c, 0x45, 0x0c, 0x00, 0x39, 0xc0, 0x3a, 0xc0, 0x4b, 0xc0, 0x5c,
0xc0, 0x61, 0xc0, 0x66, 0xc0, 0x6b, 0xc0, 0x70, 0xc0, 0x75, 0xc0, 0x7a,
0xc0, 0x7f, 0xc0, 0x84, 0xc0, 0x89, 0xc0, 0x95, 0xc0, 0xc9, 0x47, 0xed,
0xfe, 0xfe, 0x01, 0xcd, 0x9a, 0xc0, 0xc8, 0x4f, 0x78, 0xed, 0xfe, 0xfe,
0x03, 0x79, 0xc9, 0x47, 0xed, 0xfe, 0xfe, 0x02, 0xcd, 0x9a, 0xc0, 0xc8,
0x4f, 0x78, 0xed, 0xfe, 0xfe, 0x04, 0x79, 0xc9, 0xed, 0xfe, 0xfe, 0x03,
0xc9, 0xed, 0xfe, 0xfe, 0x04, 0xc9, 0xed, 0xfe, 0xfe, 0x05, 0xc9, 0xed,
0xfe, 0xfe, 0x06, 0xc9, 0xed, 0xfe, 0xfe, 0x07, 0xc9, 0xed, 0xfe, 0xfe,
0x08, 0xc9, 0xed, 0xfe, 0xfe, 0x09, 0xc9, 0xed, 0xfe, 0xfe, 0x0a, 0xc9,
0xed, 0xfe, 0xfe, 0x0b, 0xc9, 0xed, 0xfe, 0xfe, 0x0c, 0x11, 0x16, 0xc0,
0x0e, 0x01, 0xf7, 0x13, 0xc9, 0xed, 0xfe, 0xfe, 0x0d, 0xc9, 0xa7, 0xc0,
0xc5, 0x78, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0xf7, 0x1b, 0xc1, 0xa7,
0xc9
};
}

View File

@@ -0,0 +1,52 @@
//
// HostFSHandler.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Storage/FileBundle/FileBundle.hpp"
#include <cstdint>
#include <memory>
#include <unordered_map>
#include <vector>
namespace Enterprise {
struct HostFSHandler {
struct MemoryAccessor {
// Accessors that read from however the Z80's 64kb is currently laid out.
virtual uint8_t hostfs_read(uint16_t) = 0;
// virtual void hostfs_write(uint16_t, uint8_t) = 0;
// Accessors that read from 'user' address space, i.e. the 64kb Z80 address space as currently
// mapped according to the user's preference.
virtual uint8_t hostfs_user_read(uint16_t) = 0;
virtual void hostfs_user_write(uint16_t, uint8_t) = 0;
};
HostFSHandler(MemoryAccessor &);
/// Perform the internally-defined @c function given other provided state.
/// These function calls mostly align with those in EXOSCodes.hpp
void perform(uint8_t function, uint8_t &a, uint16_t &bc, uint16_t &de);
/// Sets the bundle of files on which this handler should operate.
void set_file_bundle(std::shared_ptr<Storage::FileBundle::FileBundle> bundle);
/// @returns A suitable in-client filing system ROM.
std::vector<uint8_t> rom();
private:
MemoryAccessor &accessor_;
std::shared_ptr<Storage::FileBundle::FileBundle> bundle_;
using ChannelHandler = uint8_t;
std::unordered_map<ChannelHandler, Storage::FileHolder> channels_;
};
};

View File

@@ -1148,6 +1148,12 @@
4BCE0060227D39AB000CA200 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCE005E227D39AB000CA200 /* Video.cpp */; };
4BCE1DF125D4C3FA00AE7A2B /* Bus.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCE1DEF25D4C3FA00AE7A2B /* Bus.cpp */; };
4BCE1DF225D4C3FA00AE7A2B /* Bus.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCE1DEF25D4C3FA00AE7A2B /* Bus.cpp */; };
4BCF1ACF2ECE759000109999 /* FileBundle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1ACD2ECE759000109999 /* FileBundle.cpp */; };
4BCF1AD02ECE759000109999 /* FileBundle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1ACD2ECE759000109999 /* FileBundle.cpp */; };
4BCF1AD12ECE759000109999 /* FileBundle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1ACD2ECE759000109999 /* FileBundle.cpp */; };
4BCF1AD52ECF884100109999 /* HostFSHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1AD42ECF884100109999 /* HostFSHandler.cpp */; };
4BCF1AD62ECF884100109999 /* HostFSHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1AD42ECF884100109999 /* HostFSHandler.cpp */; };
4BCF1AD72ECF884100109999 /* HostFSHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1AD42ECF884100109999 /* HostFSHandler.cpp */; };
4BCF1FA41DADC3DD0039D2E7 /* Oric.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1FA21DADC3DD0039D2E7 /* Oric.cpp */; };
4BD0FBC3233706A200148981 /* CSApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BD0FBC2233706A200148981 /* CSApplication.m */; };
4BD191F52191180E0042E144 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD191F22191180E0042E144 /* ScanTarget.cpp */; };
@@ -2425,6 +2431,11 @@
4BCE005F227D39AB000CA200 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = "<group>"; };
4BCE1DEF25D4C3FA00AE7A2B /* Bus.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Bus.cpp; sourceTree = "<group>"; };
4BCE1DF025D4C3FA00AE7A2B /* Bus.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Bus.hpp; sourceTree = "<group>"; };
4BCF1ACC2ECE759000109999 /* FileBundle.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = FileBundle.hpp; sourceTree = "<group>"; };
4BCF1ACD2ECE759000109999 /* FileBundle.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = FileBundle.cpp; sourceTree = "<group>"; };
4BCF1AD22ECF743500109999 /* EXOSCodes.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = EXOSCodes.hpp; sourceTree = "<group>"; };
4BCF1AD32ECF884100109999 /* HostFSHandler.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = HostFSHandler.hpp; sourceTree = "<group>"; };
4BCF1AD42ECF884100109999 /* HostFSHandler.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = HostFSHandler.cpp; sourceTree = "<group>"; };
4BCF1FA21DADC3DD0039D2E7 /* Oric.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Oric.cpp; sourceTree = "<group>"; };
4BCF1FA31DADC3DD0039D2E7 /* Oric.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Oric.hpp; sourceTree = "<group>"; };
4BD060A51FE49D3C006E14BE /* Speaker.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Speaker.hpp; sourceTree = "<group>"; };
@@ -2700,11 +2711,14 @@
4BFEA2ED2682A7B900EBF94C /* Dave.cpp */,
4B051CA12676F52200CA44E8 /* Enterprise.cpp */,
4B051CB42680158600CA44E8 /* EXDos.cpp */,
4BCF1AD42ECF884100109999 /* HostFSHandler.cpp */,
4B051CAE267C1CA200CA44E8 /* Keyboard.cpp */,
4B051CAA26783E2000CA44E8 /* Nick.cpp */,
4BFEA2EE2682A7B900EBF94C /* Dave.hpp */,
4B051CA02676F52200CA44E8 /* Enterprise.hpp */,
4B051CB52680158600CA44E8 /* EXDos.hpp */,
4BCF1AD22ECF743500109999 /* EXOSCodes.hpp */,
4BCF1AD32ECF884100109999 /* HostFSHandler.hpp */,
4B051CAF267C1CA200CA44E8 /* Keyboard.hpp */,
4B051CAB26783E2000CA44E8 /* Nick.hpp */,
);
@@ -3587,6 +3601,7 @@
4BEE0A691D72496600532C7B /* Cartridge */,
4B8805F81DCFF6CD003085B1 /* Data */,
4BAB62AA1D3272D200DF5BA0 /* Disk */,
4BCF1ACE2ECE759000109999 /* FileBundle */,
4B6AAEA1230E3E1D0078E864 /* MassStorage */,
4B8DD3832634D37E00B3C866 /* State */,
4B69FB3A1C4D908A00B5F0AA /* Tape */,
@@ -5289,6 +5304,15 @@
path = ADB;
sourceTree = "<group>";
};
4BCF1ACE2ECE759000109999 /* FileBundle */ = {
isa = PBXGroup;
children = (
4BCF1ACC2ECE759000109999 /* FileBundle.hpp */,
4BCF1ACD2ECE759000109999 /* FileBundle.cpp */,
);
path = FileBundle;
sourceTree = "<group>";
};
4BCF1FA51DADC3E10039D2E7 /* Oric */ = {
isa = PBXGroup;
children = (
@@ -6197,6 +6221,7 @@
4B65086122F4CFE0009C1100 /* Keyboard.cpp in Sources */,
4BBB70A9202014E2002FE009 /* MultiProducer.cpp in Sources */,
4B2E86BF25D74F160024F1E9 /* Mouse.cpp in Sources */,
4BCF1AD12ECE759000109999 /* FileBundle.cpp in Sources */,
4B6ED2F1208E2F8A0047B343 /* WOZ.cpp in Sources */,
4B5D5C9825F56FC7001B4623 /* Spectrum.cpp in Sources */,
4B7C681727517A59001671EC /* Sprites.cpp in Sources */,
@@ -6339,6 +6364,7 @@
4B055AC41FAE9AE80060FFFF /* Keyboard.cpp in Sources */,
4B8DF506254E3C9D00F3433C /* ADB.cpp in Sources */,
4B055A941FAE85B50060FFFF /* CommodoreROM.cpp in Sources */,
4BCF1AD72ECF884100109999 /* HostFSHandler.cpp in Sources */,
4BBB70A5202011C2002FE009 /* MultiMediaTarget.cpp in Sources */,
4B8318BC22D3E588006DB630 /* DisplayMetrics.cpp in Sources */,
4BEDA40E25B2844B000C2DBD /* Decoder.cpp in Sources */,
@@ -6432,6 +6458,7 @@
4B1082C42C1F5E7D00B07C5D /* CSL.cpp in Sources */,
4B0ACC3023775819008902D0 /* TIASound.cpp in Sources */,
4B7136861F78724F008B8ED9 /* Encoder.cpp in Sources */,
4BCF1AD62ECF884100109999 /* HostFSHandler.cpp in Sources */,
4B0E04EA1FC9E5DA00F43484 /* CAS.cpp in Sources */,
4B7A90ED20410A85008514A2 /* StaticAnalyser.cpp in Sources */,
4B58601E1F806AB200AEE2E3 /* MFMSectorDump.cpp in Sources */,
@@ -6497,6 +6524,7 @@
4BEDA3BF25B25563000C2DBD /* Decoder.cpp in Sources */,
4B051C95266EF50200CA44E8 /* AppleIIController.swift in Sources */,
4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */,
4BCF1ACF2ECE759000109999 /* FileBundle.cpp in Sources */,
4BE8EB6625C750B50040BC40 /* DAT.cpp in Sources */,
4BBFFEE61F7B27F1005F3FEB /* TrackSerialiser.cpp in Sources */,
4B8855A52E84D51B00E251DD /* SAA5050.cpp in Sources */,
@@ -6857,8 +6885,10 @@
4B778F4023A5F1910000D260 /* z8530.cpp in Sources */,
4B778EFD23A5EB8E0000D260 /* AppleDSK.cpp in Sources */,
4B7752B728217EF40073E2C5 /* Chipset.cpp in Sources */,
4BCF1AD02ECE759000109999 /* FileBundle.cpp in Sources */,
4B06AAF72C64606E0034D014 /* DiskII.cpp in Sources */,
4B778EFB23A5EB7E0000D260 /* HFE.cpp in Sources */,
4BCF1AD52ECF884100109999 /* HostFSHandler.cpp in Sources */,
4BC751B21D157E61006C31D9 /* 6522Tests.swift in Sources */,
4B0DA67D282DCDF300C12F17 /* Instruction.cpp in Sources */,
4B06AAE12C645F8B0034D014 /* Video.cpp in Sources */,

View File

@@ -68,6 +68,7 @@ class MachineDocument:
var fileObserver: CSFileContentChangeObserver?
override func read(from url: URL, ofType typeName: String) throws {
if let analyser = CSStaticAnalyser(fileAt: url) {
checkPermisions(analyser.mediaSet)
self.displayName = analyser.displayName
self.configureAs(analyser)
self.fileObserver = CSFileContentChangeObserver.init(url: url, handler: {
@@ -332,6 +333,7 @@ class MachineDocument:
private func insertFile(_ URL: URL) {
// Try to insert media.
let mediaSet = CSMediaSet(fileAt: URL)
checkPermisions(mediaSet)
if !mediaSet.empty {
mediaSet.apply(to: self.machine)
return
@@ -347,6 +349,10 @@ class MachineDocument:
}
}
private func checkPermisions(_ mediaSet: CSMediaSet) {
mediaSet.addPermissionHandler()
}
// MARK: - Input Management.
/// Upon a resign key, immediately releases all ongoing input mechanisms any currently pressed keys,

View File

@@ -807,6 +807,29 @@
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>com</string>
<string>bas</string>
</array>
<key>CFBundleTypeIconFile</key>
<string></string>
<key>CFBundleTypeName</key>
<string>Enterprise Executable</string>
<key>CFBundleTypeOSTypes</key>
<array>
<string>????</string>
</array>
<key>CFBundleTypeRole</key>
<string>Viewer</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>
@@ -821,11 +844,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>25.11.07</string>
<string>25.11.26</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>25.11.07</string>
<string>25.11.26</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.entertainment</string>
<key>LSMinimumSystemVersion</key>

View File

@@ -12,6 +12,16 @@ NS_ASSUME_NONNULL_BEGIN
@class CSMachine;
@interface CSMediaSet : NSObject
- (instancetype)initWithFileAtURL:(NSURL *)url;
- (void)applyToMachine:(CSMachine *)machine;
- (void)addPermissionHandler;
@property(nonatomic, readonly) BOOL empty;
@end
typedef NS_ENUM(NSInteger, CSMachineAmigaModel) {
CSMachineAmigaModelA500,
};
@@ -150,36 +160,71 @@ typedef int Kilobytes;
- (nullable instancetype)initWithFileAtURL:(NSURL *)url;
- (instancetype)initWithAmigaModel:(CSMachineAmigaModel)model chipMemorySize:(Kilobytes)chipMemorySize fastMemorySize:(Kilobytes)fastMemorySize;
- (instancetype)initWithAmigaModel:(CSMachineAmigaModel)model
chipMemorySize:(Kilobytes)chipMemorySize
fastMemorySize:(Kilobytes)fastMemorySize;
- (instancetype)initWithAmstradCPCModel:(CSMachineCPCModel)model;
- (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model diskController:(CSMachineAppleIIDiskController)diskController hasMockingboard:(BOOL)hasMockingboard;
- (instancetype)initWithAppleIIgsModel:(CSMachineAppleIIgsModel)model memorySize:(Kilobytes)memorySize;
- (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model
diskController:(CSMachineAppleIIDiskController)diskController
hasMockingboard:(BOOL)hasMockingboard;
- (instancetype)initWithAppleIIgsModel:(CSMachineAppleIIgsModel)model
memorySize:(Kilobytes)memorySize;
- (instancetype)initWithArchimedesModel:(CSMachineArchimedesModel)model;
- (instancetype)initWithAtariSTMemorySize:(Kilobytes)memorySize;
- (instancetype)initWithBBCMicroDFS:(BOOL)dfs adfs:(BOOL)adfs sidewaysRAM:(BOOL)sidewaysRAM secondProcessor:(CSMachineBBCMicroSecondProcessor)secondProcessor;
- (instancetype)initWithCommodoreTEDModel:(CSMachineCommodoreTEDModel)model hasC1541:(BOOL)hasC1541;
- (instancetype)initWithElectronDFS:(BOOL)dfs adfs:(BOOL)adfs ap6:(BOOL)ap6 sidewaysRAM:(BOOL)sidewaysRAM;
- (instancetype)initWithEnterpriseModel:(CSMachineEnterpriseModel)model speed:(CSMachineEnterpriseSpeed)speed exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion dos:(CSMachineEnterpriseDOS)dos;
- (instancetype)initWithBBCMicroDFS:(BOOL)dfs
adfs:(BOOL)adfs
sidewaysRAM:(BOOL)sidewaysRAM
beebSID:(BOOL)beebSID
secondProcessor:(CSMachineBBCMicroSecondProcessor)secondProcessor;
- (instancetype)initWithCommodoreTEDModel:(CSMachineCommodoreTEDModel)model
hasC1541:(BOOL)hasC1541;
- (instancetype)initWithElectronDFS:(BOOL)dfs
adfs:(BOOL)adfs
ap6:(BOOL)ap6
sidewaysRAM:(BOOL)sidewaysRAM;
- (instancetype)initWithEnterpriseModel:(CSMachineEnterpriseModel)model
speed:(CSMachineEnterpriseSpeed)speed
exosVersion:(CSMachineEnterpriseEXOS)exosVersion
basicVersion:(CSMachineEnterpriseBASIC)basicVersion
dos:(CSMachineEnterpriseDOS)dos
exposedLocalPath:(nullable NSURL *)path;
- (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model;
- (instancetype)initWithMSXModel:(CSMachineMSXModel)model region:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive hasMSXMUSIC:(BOOL)hasMSXMUSIC;
- (instancetype)initWithOricModel:(CSMachineOricModel)model diskInterface:(CSMachineOricDiskInterface)diskInterface;
- (instancetype)initWithMSXModel:(CSMachineMSXModel)model
region:(CSMachineMSXRegion)region
hasDiskDrive:(BOOL)hasDiskDrive
hasMSXMUSIC:(BOOL)hasMSXMUSIC;
- (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)initWithVic20Region:(CSMachineVic20Region)region
memorySize:(Kilobytes)memorySize
hasC1540:(BOOL)hasC1540;
- (instancetype)initWithZX80MemorySize:(Kilobytes)memorySize
useZX81ROM:(BOOL)useZX81ROM;
- (instancetype)initWithZX81MemorySize:(Kilobytes)memorySize;
- (instancetype)initWithPCCompatibleSpeed:(CSPCCompatibleSpeed)speed videoAdaptor:(CSPCCompatibleVideoAdaptor)adaptor;
- (instancetype)initWithPCCompatibleSpeed:(CSPCCompatibleSpeed)speed
videoAdaptor:(CSPCCompatibleVideoAdaptor)adaptor;
@property(nonatomic, readonly, nullable) NSString *optionsNibName;
@property(nonatomic, readonly) NSString *displayName;
@end
@interface CSMediaSet : NSObject
- (instancetype)initWithFileAtURL:(NSURL *)url;
- (void)applyToMachine:(CSMachine *)machine;
@property(nonatomic, readonly) BOOL empty;
@property(nonatomic, readonly, nonnull) CSMediaSet *mediaSet;
@end

View File

@@ -28,8 +28,164 @@
#include "Analyser/Static/ZX8081/Target.hpp"
#include "Analyser/Static/ZXSpectrum/Target.hpp"
#include "Storage/FileBundle/FileBundle.hpp"
#import "Clock_Signal-Swift.h"
namespace {
struct PermissionDelegate: public Storage::FileBundle::FileBundle::PermissionDelegate {
void validate_open(Storage::FileBundle::FileBundle &bundle, const std::string &path, const Storage::FileMode mode) {
NSData *bookmarkData;
NSString *stringPath = [NSString stringWithUTF8String:path.c_str()];
NSURL *url = [NSURL fileURLWithPath:stringPath isDirectory:NO];
NSError *error;
// Check for and possibly apply an existing bookmark.
NSString *bookmarkKey = [[url URLByDeletingLastPathComponent] absoluteString];
bookmarkData = [[NSUserDefaults standardUserDefaults] objectForKey:bookmarkKey];
if(bookmarkData) {
NSURL *accessURL =
[NSURL
URLByResolvingBookmarkData:bookmarkData
options:NSURLBookmarkResolutionWithSecurityScope | NSURLBookmarkResolutionWithoutUI
relativeToURL:nil
bookmarkDataIsStale:nil
error:nil];
[accessURL startAccessingSecurityScopedResource];
}
// If the file exists can now be accessed, no further action required.
NSFileHandle *file = [&]() {
switch(mode) {
case Storage::FileMode::ReadWrite: {
NSFileHandle *updating = [NSFileHandle fileHandleForUpdatingURL:url error:&error];
if(updating) return updating;
[[fallthrough]];
}
default:
case Storage::FileMode::Read: return [NSFileHandle fileHandleForReadingFromURL:url error:&error];
case Storage::FileMode::Rewrite: return [NSFileHandle fileHandleForWritingToURL:url error:&error];
}
}();
// Managed to open the file: that's enough.
if(file) {
return;
}
// Otherwise: if not being opened exclusively for reading, see whether the file can be created.
if(
error.domain == NSCocoaErrorDomain &&
error.code == NSFileNoSuchFileError &&
mode != Storage::FileMode::Read
) {
NSFileManager *manager = [NSFileManager defaultManager];
if([manager createFileAtPath:url.path contents:nil attributes:nil]) {
[manager removeItemAtPath:url.path error:&error];
return;
}
}
// Failing that, ask the user for permission and keep the bookmark.
__block NSURL *selectedURL;
// Ask the user for permission.
dispatch_sync(dispatch_get_main_queue(), ^{
NSOpenPanel *request = [NSOpenPanel openPanel];
request.prompt = @"Grant Permission";
request.message = @"Please Grant Permission For Full Folder Access";
request.canChooseFiles = NO;
request.allowsMultipleSelection = NO;
request.canChooseDirectories = YES;
[request setDirectoryURL:[url URLByDeletingLastPathComponent]];
request.accessoryView = [NSTextField labelWithString:[&] {
const auto key_file = bundle.key_file();
if(key_file) {
return [NSString stringWithFormat:
@"Clock Signal cannot access your files without explicit permission but "
@"%s is trying to use additional files in its folder.\n"
@"Please select 'Grant Permission' if you are willing to let it to do so.",
key_file->c_str()
];
} else {
assert(bundle.base_path().has_value());
return [NSString stringWithFormat:
@"Clock Signal cannot access your files without explicit permission but "
@"your emulated machine is trying to use additional files from %s.\n"
@"Please select 'Grant Permission' if you are willing to let it to do so.",
bundle.base_path()->c_str()
];
}
}()];
request.accessoryViewDisclosed = YES;
[request runModal];
selectedURL = request.URL;
});
// Store bookmark data for potential later retrieval.
// That amounts to this application remembering the user's permission.
error = nil;
[[NSUserDefaults standardUserDefaults]
setObject:[selectedURL
bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error]
forKey:bookmarkKey];
}
void validate_erase(Storage::FileBundle::FileBundle &, const std::string &) {
// Currently a no-op, as it so happens that the only machine that currently
// uses a file bundle is the Enterprise, and its semantics involve opening
// a file before it can be erased.
}
};
PermissionDelegate permission_delegate;
}
@implementation CSMediaSet {
Analyser::Static::Media _media;
}
- (instancetype)initWithMedia:(Analyser::Static::Media)media {
self = [super init];
if(self) {
_media = media;
}
return self;
}
- (instancetype)initWithFileAtURL:(NSURL *)url {
self = [super init];
if(self) {
_media = Analyser::Static::GetMedia([url fileSystemRepresentation]);
}
return self;
}
- (BOOL)empty {
return _media.empty();
}
- (void)applyToMachine:(CSMachine *)machine {
[machine applyMedia:_media];
}
- (void)addPermissionHandler {
for(const auto &bundle: _media.file_bundles) {
bundle->set_permission_delegate(&permission_delegate);
}
}
@end
@implementation CSStaticAnalyser {
Analyser::Static::TargetList _targets;
}
@@ -51,7 +207,10 @@
// MARK: - Machine-based Initialisers
- (instancetype)initWithAmigaModel:(CSMachineAmigaModel)model chipMemorySize:(Kilobytes)chipMemorySize fastMemorySize:(Kilobytes)fastMemorySize {
- (instancetype)initWithAmigaModel:(CSMachineAmigaModel)model
chipMemorySize:(Kilobytes)chipMemorySize
fastMemorySize:(Kilobytes)fastMemorySize
{
self = [super init];
if(self) {
using Target = Analyser::Static::Amiga::Target;
@@ -93,7 +252,10 @@
return self;
}
- (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model diskController:(CSMachineAppleIIDiskController)diskController hasMockingboard:(BOOL)hasMockingboard {
- (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model
diskController:(CSMachineAppleIIDiskController)diskController
hasMockingboard:(BOOL)hasMockingboard
{
self = [super init];
if(self) {
using Target = Analyser::Static::AppleII::Target;
@@ -116,7 +278,9 @@
return self;
}
- (instancetype)initWithAppleIIgsModel:(CSMachineAppleIIgsModel)model memorySize:(Kilobytes)memorySize {
- (instancetype)initWithAppleIIgsModel:(CSMachineAppleIIgsModel)model
memorySize:(Kilobytes)memorySize
{
self = [super init];
if(self) {
using Target = Analyser::Static::AppleIIgs::Target;
@@ -160,13 +324,19 @@
return self;
}
- (instancetype)initWithBBCMicroDFS:(BOOL)dfs adfs:(BOOL)adfs sidewaysRAM:(BOOL)sidewaysRAM secondProcessor:(CSMachineBBCMicroSecondProcessor)secondProcessor {
- (instancetype)initWithBBCMicroDFS:(BOOL)dfs
adfs:(BOOL)adfs
sidewaysRAM:(BOOL)sidewaysRAM
beebSID:(BOOL)beebSID
secondProcessor:(CSMachineBBCMicroSecondProcessor)secondProcessor
{
self = [super init];
if(self) {
using Target = Analyser::Static::Acorn::BBCMicroTarget;
auto target = std::make_unique<Target>();
target->has_1770dfs = dfs;
target->has_adfs = adfs;
target->has_beebsid = beebSID;
target->has_sideways_ram = sidewaysRAM;
switch(secondProcessor) {
@@ -179,7 +349,9 @@
return self;
}
- (instancetype)initWithCommodoreTEDModel:(CSMachineCommodoreTEDModel)model hasC1541:(BOOL)hasC1541 {
- (instancetype)initWithCommodoreTEDModel:(CSMachineCommodoreTEDModel)model
hasC1541:(BOOL)hasC1541
{
self = [super init];
if(self) {
using Target = Analyser::Static::Commodore::Plus4Target;
@@ -190,7 +362,11 @@
return self;
}
- (instancetype)initWithElectronDFS:(BOOL)dfs adfs:(BOOL)adfs ap6:(BOOL)ap6 sidewaysRAM:(BOOL)sidewaysRAM {
- (instancetype)initWithElectronDFS:(BOOL)dfs
adfs:(BOOL)adfs
ap6:(BOOL)ap6
sidewaysRAM:(BOOL)sidewaysRAM
{
self = [super init];
if(self) {
auto target = std::make_unique<Analyser::Static::Acorn::ElectronTarget>();
@@ -203,7 +379,13 @@
return self;
}
- (instancetype)initWithEnterpriseModel:(CSMachineEnterpriseModel)model speed:(CSMachineEnterpriseSpeed)speed exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion dos:(CSMachineEnterpriseDOS)dos {
- (instancetype)initWithEnterpriseModel:(CSMachineEnterpriseModel)model
speed:(CSMachineEnterpriseSpeed)speed
exosVersion:(CSMachineEnterpriseEXOS)exosVersion
basicVersion:(CSMachineEnterpriseBASIC)basicVersion
dos:(CSMachineEnterpriseDOS)dos
exposedLocalPath:(nullable NSURL *)path
{
self = [super init];
if(self) {
using Target = Analyser::Static::Enterprise::Target;
@@ -243,6 +425,12 @@
case CSMachineEnterpriseDOSNone: target->dos = Target::DOS::None; break;
}
if(path) {
const auto bundle = std::make_shared<Storage::FileBundle::LocalFSFileBundle>(path.path.UTF8String);
bundle->set_permission_delegate(&permission_delegate);
target->media.file_bundles.push_back(std::move(bundle));
}
_targets.push_back(std::move(target));
}
return self;
@@ -268,7 +456,11 @@
return self;
}
- (instancetype)initWithMSXModel:(CSMachineMSXModel)model region:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive hasMSXMUSIC:(BOOL)hasMSXMUSIC {
- (instancetype)initWithMSXModel:(CSMachineMSXModel)model
region:(CSMachineMSXRegion)region
hasDiskDrive:(BOOL)hasDiskDrive
hasMSXMUSIC:(BOOL)hasMSXMUSIC
{
self = [super init];
if(self) {
using Target = Analyser::Static::MSX::Target;
@@ -289,7 +481,9 @@
return self;
}
- (instancetype)initWithOricModel:(CSMachineOricModel)model diskInterface:(CSMachineOricDiskInterface)diskInterface {
- (instancetype)initWithOricModel:(CSMachineOricModel)model
diskInterface:(CSMachineOricDiskInterface)diskInterface
{
self = [super init];
if(self) {
using Target = Analyser::Static::Oric::Target;
@@ -311,7 +505,10 @@
return self;
}
- (instancetype)initWithPCCompatibleSpeed:(CSPCCompatibleSpeed)speed videoAdaptor:(CSPCCompatibleVideoAdaptor)adaptor {
- (instancetype)initWithPCCompatibleSpeed:(CSPCCompatibleSpeed)speed
videoAdaptor:(CSPCCompatibleVideoAdaptor)adaptor
{
self = [super init];
if(self) {
using Target = Analyser::Static::PCCompatible::Target;
@@ -347,7 +544,10 @@
return self;
}
- (instancetype)initWithVic20Region:(CSMachineVic20Region)region memorySize:(Kilobytes)memorySize hasC1540:(BOOL)hasC1540 {
- (instancetype)initWithVic20Region:(CSMachineVic20Region)region
memorySize:(Kilobytes)memorySize
hasC1540:(BOOL)hasC1540
{
self = [super init];
if(self) {
using Target = Analyser::Static::Commodore::Vic20Target;
@@ -381,7 +581,9 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
}
}
- (instancetype)initWithZX80MemorySize:(Kilobytes)memorySize useZX81ROM:(BOOL)useZX81ROM {
- (instancetype)initWithZX80MemorySize:(Kilobytes)memorySize
useZX81ROM:(BOOL)useZX81ROM
{
self = [super init];
if(self) {
using Target = Analyser::Static::ZX8081::Target;
@@ -438,26 +640,12 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
return _targets;
}
@end
@implementation CSMediaSet {
Analyser::Static::Media _media;
}
- (instancetype)initWithFileAtURL:(NSURL *)url {
self = [super init];
if(self) {
_media = Analyser::Static::GetMedia([url fileSystemRepresentation]);
- (nonnull CSMediaSet *)mediaSet {
Analyser::Static::Media net;
for(const auto &target: _targets) {
net += target->media;
}
return self;
}
- (void)applyToMachine:(CSMachine *)machine {
[machine applyMedia:_media];
}
- (BOOL)empty {
return _media.empty();
return [[CSMediaSet alloc] initWithMedia:net];
}
@end

View File

@@ -440,15 +440,15 @@ Gw
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="lzo-8g-o4S">
<rect key="frame" x="18" y="211" width="284" height="18"/>
<buttonCell key="cell" type="check" title="Fill unused ROM banks with sideways RAM" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="JEz-eK-uWp">
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="UgW-vD-OSo">
<rect key="frame" x="18" y="211" width="110" height="18"/>
<buttonCell key="cell" type="check" title="With BeebSID" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="ucb-pL-9rx">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="y7H-nu-CPH">
<rect key="frame" x="20" y="174" width="118" height="16"/>
<rect key="frame" x="20" y="152" width="118" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Second Processor:" id="11b-Ml-o3Y">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -456,7 +456,7 @@ Gw
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yhA-Dw-hqn">
<rect key="frame" x="141" y="168" width="81" height="25"/>
<rect key="frame" x="141" y="146" width="81" height="25"/>
<popUpButtonCell key="cell" type="push" title="None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Rpc-1u-8X2" id="vk3-Qo-uxV">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="message"/>
@@ -469,18 +469,28 @@ Gw
</menu>
</popUpButtonCell>
</popUpButton>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="lzo-8g-o4S">
<rect key="frame" x="18" y="189" width="284" height="18"/>
<buttonCell key="cell" type="check" title="Fill unused ROM banks with sideways RAM" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="JEz-eK-uWp">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
</subviews>
<constraints>
<constraint firstItem="945-wU-JOH" firstAttribute="leading" secondItem="SRc-2D-95G" secondAttribute="leading" constant="20" symbolic="YES" id="1iM-70-oZq"/>
<constraint firstItem="yhA-Dw-hqn" firstAttribute="top" secondItem="lzo-8g-o4S" secondAttribute="bottom" constant="20" symbolic="YES" id="28I-GC-l4K"/>
<constraint firstItem="y7H-nu-CPH" firstAttribute="leading" secondItem="SRc-2D-95G" secondAttribute="leading" constant="22" id="3yR-gS-ciE"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="yhA-Dw-hqn" secondAttribute="trailing" constant="20" symbolic="YES" id="8V6-jM-LW7"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="UgW-vD-OSo" secondAttribute="trailing" constant="20" symbolic="YES" id="8v9-Ly-7XU"/>
<constraint firstItem="y7H-nu-CPH" firstAttribute="centerY" secondItem="yhA-Dw-hqn" secondAttribute="centerY" id="IUr-qS-L4l"/>
<constraint firstItem="lzo-8g-o4S" firstAttribute="top" secondItem="UgW-vD-OSo" secondAttribute="bottom" constant="6" symbolic="YES" id="LpK-aW-KOr"/>
<constraint firstItem="UgW-vD-OSo" firstAttribute="leading" secondItem="SRc-2D-95G" secondAttribute="leading" constant="20" symbolic="YES" id="N2b-ZW-ffU"/>
<constraint firstItem="JqM-IK-FMP" firstAttribute="leading" secondItem="SRc-2D-95G" secondAttribute="leading" constant="20" symbolic="YES" id="NfY-dE-aJw"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="yhA-Dw-hqn" secondAttribute="bottom" constant="20" symbolic="YES" id="Rg8-zf-0ep"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="lzo-8g-o4S" secondAttribute="trailing" constant="20" symbolic="YES" id="X3p-qJ-ENH"/>
<constraint firstItem="UgW-vD-OSo" firstAttribute="top" secondItem="945-wU-JOH" secondAttribute="bottom" constant="6" symbolic="YES" id="YqY-Yk-Puc"/>
<constraint firstItem="lzo-8g-o4S" firstAttribute="leading" secondItem="SRc-2D-95G" secondAttribute="leading" constant="20" symbolic="YES" id="b5a-SX-2ty"/>
<constraint firstItem="lzo-8g-o4S" firstAttribute="top" secondItem="945-wU-JOH" secondAttribute="bottom" constant="6" symbolic="YES" id="d1Y-Ia-IjQ"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="945-wU-JOH" secondAttribute="trailing" constant="20" symbolic="YES" id="dmY-PV-ap4"/>
<constraint firstItem="JqM-IK-FMP" firstAttribute="top" secondItem="SRc-2D-95G" secondAttribute="top" constant="20" symbolic="YES" id="ggl-QH-mV4"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="JqM-IK-FMP" secondAttribute="trailing" constant="20" symbolic="YES" id="mvO-UZ-BtT"/>
@@ -562,11 +572,11 @@ Gw
</tabViewItem>
<tabViewItem label="Enterprise" identifier="enterprise" id="zhO-EO-wUe">
<view key="view" id="1cs-PX-RAH">
<rect key="frame" x="10" y="7" width="400" height="274"/>
<rect key="frame" x="10" y="7" width="400" height="292"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PhH-bu-pb5">
<rect key="frame" x="80" y="230" width="129" height="25"/>
<rect key="frame" x="80" y="248" width="129" height="25"/>
<popUpButtonCell key="cell" type="push" title="Enterprise 128" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="128" imageScaling="axesIndependently" inset="2" selectedItem="roH-nL-f8o" id="z9O-XC-XBv">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="message"/>
@@ -580,7 +590,7 @@ Gw
</popUpButtonCell>
</popUpButton>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Lfl-5c-b8j">
<rect key="frame" x="67" y="200" width="77" height="25"/>
<rect key="frame" x="67" y="218" width="77" height="25"/>
<popUpButtonCell key="cell" type="push" title="4 Mhz" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="4" imageScaling="axesIndependently" inset="2" selectedItem="5N6-tD-uN7" id="BU2-NJ-Kii">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="message"/>
@@ -593,7 +603,7 @@ Gw
</popUpButtonCell>
</popUpButton>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nen-Za-7zH">
<rect key="frame" x="64" y="170" width="107" height="25"/>
<rect key="frame" x="64" y="188" width="107" height="25"/>
<popUpButtonCell key="cell" type="push" title="Version 2.1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="21" imageScaling="axesIndependently" inset="2" selectedItem="Qja-xZ-wVM" id="xGG-ri-8Sb">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="message"/>
@@ -607,7 +617,7 @@ Gw
</popUpButtonCell>
</popUpButton>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hIr-GH-7xi">
<rect key="frame" x="67" y="140" width="105" height="25"/>
<rect key="frame" x="67" y="158" width="105" height="25"/>
<popUpButtonCell key="cell" type="push" title="Version 2.1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="21" imageScaling="axesIndependently" inset="2" selectedItem="TME-cv-Jh1" id="9mQ-GW-lq9">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="message"/>
@@ -622,7 +632,7 @@ Gw
</popUpButtonCell>
</popUpButton>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="syE-e7-TjU">
<rect key="frame" x="57" y="110" width="83" height="25"/>
<rect key="frame" x="57" y="128" width="83" height="25"/>
<popUpButtonCell key="cell" type="push" title="EXDOS" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="1" imageScaling="axesIndependently" inset="2" selectedItem="8rP-2w-PdU" id="NvO-Zm-2Rq">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="message"/>
@@ -635,7 +645,7 @@ Gw
</popUpButtonCell>
</popUpButton>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ykc-W1-YaS">
<rect key="frame" x="18" y="176" width="43" height="16"/>
<rect key="frame" x="18" y="194" width="43" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="EXOS:" id="gUC-PN-zVL">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -643,7 +653,7 @@ Gw
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="frx-nk-c3P">
<rect key="frame" x="18" y="236" width="59" height="16"/>
<rect key="frame" x="18" y="254" width="59" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Machine:" id="uTv-hH-mIC">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -651,7 +661,7 @@ Gw
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dzd-tH-BjX">
<rect key="frame" x="18" y="146" width="46" height="16"/>
<rect key="frame" x="18" y="164" width="46" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="BASIC:" id="ai1-oR-X6Y">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -659,7 +669,7 @@ Gw
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pxr-Bq-yh0">
<rect key="frame" x="18" y="116" width="36" height="16"/>
<rect key="frame" x="18" y="134" width="36" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="DOS:" id="NFk-cp-DfS">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -667,17 +677,38 @@ Gw
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="rHr-bh-QMV">
<rect key="frame" x="17" y="206" width="47" height="16"/>
<rect key="frame" x="17" y="224" width="47" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Speed:" id="sAw-C9-Sf7">
<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>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gv6-nD-GXj">
<rect key="frame" x="18" y="107" width="207" height="18"/>
<buttonCell key="cell" type="check" title="Expose local path to machine:" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="l0h-Ih-sZi">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<pathControl verticalHuggingPriority="750" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Q4Y-IX-PrB">
<rect key="frame" x="230" y="103" width="96" height="25"/>
<pathCell key="cell" selectable="YES" editable="YES" alignment="left" pathStyle="popUp" id="gwZ-3L-JoC">
<font key="font" metaFont="system"/>
<url key="url" string="~">
<url key="baseURL" string="file:///"/>
</url>
</pathCell>
<connections>
<outlet property="delegate" destination="192-Eb-Rpg" id="o8l-Od-rKa"/>
</connections>
</pathControl>
</subviews>
<constraints>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Q4Y-IX-PrB" secondAttribute="trailing" constant="20" symbolic="YES" id="3KV-Zx-nbH"/>
<constraint firstItem="dzd-tH-BjX" firstAttribute="centerY" secondItem="hIr-GH-7xi" secondAttribute="centerY" id="3TV-RU-Kgh"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="hIr-GH-7xi" secondAttribute="trailing" constant="20" symbolic="YES" id="44v-9O-y7L"/>
<constraint firstItem="Q4Y-IX-PrB" firstAttribute="centerY" secondItem="gv6-nD-GXj" secondAttribute="centerY" id="4CW-jc-zZZ"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Lfl-5c-b8j" secondAttribute="trailing" constant="20" symbolic="YES" id="5tb-fZ-HpY"/>
<constraint firstItem="frx-nk-c3P" firstAttribute="centerY" secondItem="PhH-bu-pb5" secondAttribute="centerY" id="6Wc-aR-wuL"/>
<constraint firstItem="dzd-tH-BjX" firstAttribute="leading" secondItem="1cs-PX-RAH" secondAttribute="leading" constant="20" symbolic="YES" id="7RZ-Om-TAa"/>
@@ -685,11 +716,14 @@ Gw
<constraint firstItem="nen-Za-7zH" firstAttribute="top" secondItem="Lfl-5c-b8j" secondAttribute="bottom" constant="10" symbolic="YES" id="Df8-qv-3dY"/>
<constraint firstItem="PhH-bu-pb5" firstAttribute="leading" secondItem="frx-nk-c3P" secondAttribute="trailing" constant="8" symbolic="YES" id="ENF-TY-TQ7"/>
<constraint firstItem="nen-Za-7zH" firstAttribute="leading" secondItem="ykc-W1-YaS" secondAttribute="trailing" constant="8" symbolic="YES" id="GWR-VI-9PG"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="syE-e7-TjU" secondAttribute="bottom" constant="20" symbolic="YES" id="K3s-FA-zMB"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="gv6-nD-GXj" secondAttribute="trailing" constant="20" symbolic="YES" id="H04-Wk-kvx"/>
<constraint firstItem="Q4Y-IX-PrB" firstAttribute="leading" secondItem="gv6-nD-GXj" secondAttribute="trailing" constant="8" symbolic="YES" id="HVH-bw-Pnk"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="PhH-bu-pb5" secondAttribute="trailing" constant="20" symbolic="YES" id="LwB-ef-uF4"/>
<constraint firstItem="PhH-bu-pb5" firstAttribute="top" secondItem="1cs-PX-RAH" secondAttribute="top" constant="20" symbolic="YES" id="Myt-i4-jVq"/>
<constraint firstItem="Lfl-5c-b8j" firstAttribute="top" secondItem="PhH-bu-pb5" secondAttribute="bottom" constant="10" symbolic="YES" id="Nl3-hL-jwg"/>
<constraint firstItem="pxr-Bq-yh0" firstAttribute="leading" secondItem="1cs-PX-RAH" secondAttribute="leading" constant="20" symbolic="YES" id="Qzp-IY-Pa0"/>
<constraint firstItem="gv6-nD-GXj" firstAttribute="top" secondItem="syE-e7-TjU" secondAttribute="bottom" constant="8" symbolic="YES" id="S68-zD-nye"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="gv6-nD-GXj" secondAttribute="bottom" constant="20" symbolic="YES" id="SUq-nZ-P0W"/>
<constraint firstItem="PhH-bu-pb5" firstAttribute="leading" secondItem="frx-nk-c3P" secondAttribute="trailing" constant="8" symbolic="YES" id="T3e-u7-fiQ"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="syE-e7-TjU" secondAttribute="trailing" constant="20" symbolic="YES" id="TEX-Nw-y2K"/>
<constraint firstItem="frx-nk-c3P" firstAttribute="leading" secondItem="1cs-PX-RAH" secondAttribute="leading" constant="20" symbolic="YES" id="TgR-RR-eA1"/>
@@ -703,6 +737,7 @@ Gw
<constraint firstItem="syE-e7-TjU" firstAttribute="leading" secondItem="pxr-Bq-yh0" secondAttribute="trailing" constant="8" symbolic="YES" id="fmM-Ma-Jyu"/>
<constraint firstItem="hIr-GH-7xi" firstAttribute="leading" secondItem="dzd-tH-BjX" secondAttribute="trailing" constant="8" symbolic="YES" id="jDQ-TF-Ogf"/>
<constraint firstItem="rHr-bh-QMV" firstAttribute="centerY" secondItem="Lfl-5c-b8j" secondAttribute="centerY" id="mnp-JF-dVQ"/>
<constraint firstItem="gv6-nD-GXj" firstAttribute="leading" secondItem="1cs-PX-RAH" secondAttribute="leading" constant="20" symbolic="YES" id="uhb-Rp-Gs2"/>
<constraint firstItem="Lfl-5c-b8j" firstAttribute="leading" secondItem="rHr-bh-QMV" secondAttribute="trailing" constant="8" symbolic="YES" id="yfb-SL-v1H"/>
</constraints>
</view>
@@ -899,11 +934,11 @@ Gw
</tabViewItem>
<tabViewItem label="PC Compatible" identifier="pc" id="8cB-MZ-g3U">
<view key="view" id="gJD-vd-WWu">
<rect key="frame" x="10" y="7" width="400" height="274"/>
<rect key="frame" x="10" y="7" width="400" height="292"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="stw-i3-ikG">
<rect key="frame" x="116" y="230" width="68" height="25"/>
<rect key="frame" x="116" y="248" width="68" height="25"/>
<popUpButtonCell key="cell" type="push" title="MDA" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="axesIndependently" inset="2" selectedItem="oIy-If-5bQ" id="xz8-mu-ynU">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="message"/>
@@ -916,7 +951,7 @@ Gw
</popUpButtonCell>
</popUpButton>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="uhf-1k-ibT">
<rect key="frame" x="18" y="236" width="95" height="16"/>
<rect key="frame" x="18" y="254" width="95" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Video Adaptor:" id="ROV-EU-T3W">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -924,7 +959,7 @@ Gw
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8vF-eu-ClP">
<rect key="frame" x="18" y="206" width="47" height="16"/>
<rect key="frame" x="18" y="224" width="47" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Speed:" id="qXc-wf-5jm">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -932,7 +967,7 @@ Gw
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ci6-TG-5tW">
<rect key="frame" x="68" y="200" width="146" height="25"/>
<rect key="frame" x="68" y="218" width="146" height="25"/>
<popUpButtonCell key="cell" type="push" title="Similar to Original" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="8086" imageScaling="axesIndependently" inset="2" selectedItem="R4W-s4-KFx" id="9i0-UG-B2c">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="message"/>
@@ -1200,6 +1235,7 @@ Gw
<outlet property="appleIIgsModelButton" destination="gcS-uy-mzl" id="Jcc-jC-cV1"/>
<outlet property="atariSTMemorySizeButton" destination="QD0-qk-qCa" id="aKa-L6-2Te"/>
<outlet property="bbcADFSButton" destination="945-wU-JOH" id="fec-Lp-8F2"/>
<outlet property="bbcBeebSIDButton" destination="UgW-vD-OSo" id="k9j-Vc-dyN"/>
<outlet property="bbcDFSButton" destination="JqM-IK-FMP" id="ocY-a5-u7h"/>
<outlet property="bbcSecondProcessorButton" destination="yhA-Dw-hqn" id="lzf-f5-4iQ"/>
<outlet property="bbcSidewaysRAMButton" destination="lzo-8g-o4S" id="3P1-20-Ne7"/>
@@ -1211,7 +1247,9 @@ Gw
<outlet property="enterpriseBASICButton" destination="hIr-GH-7xi" id="fM6-It-9UO"/>
<outlet property="enterpriseDOSButton" destination="syE-e7-TjU" id="sCW-Bj-ZTW"/>
<outlet property="enterpriseEXOSButton" destination="nen-Za-7zH" id="NwS-ua-FdA"/>
<outlet property="enterpriseExposePathButton" destination="gv6-nD-GXj" id="04H-5s-e06"/>
<outlet property="enterpriseModelButton" destination="PhH-bu-pb5" id="8wD-sW-aBw"/>
<outlet property="enterprisePathControl" destination="Q4Y-IX-PrB" id="g3m-Z7-C9D"/>
<outlet property="enterpriseSpeedButton" destination="Lfl-5c-b8j" id="ClN-yN-Nbx"/>
<outlet property="machineNameTable" destination="3go-Eb-GOy" id="Ppf-S0-IP1"/>
<outlet property="machineSelector" destination="VUb-QG-x7c" id="crR-hB-jGd"/>

View File

@@ -15,7 +15,7 @@ import Cocoa
// in the interface builder easier.
//
// I accept that I'll have to rethink this again if the machine list keeps growing.
class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate, NSPathControlDelegate {
@IBOutlet var machineSelector: NSTabView!
@IBOutlet var machineNameTable: NSTableView!
@@ -39,6 +39,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
@IBOutlet var bbcDFSButton: NSButton!
@IBOutlet var bbcADFSButton: NSButton!
@IBOutlet var bbcSidewaysRAMButton: NSButton!
@IBOutlet var bbcBeebSIDButton: NSButton!
@IBOutlet var bbcSecondProcessorButton: NSPopUpButton!
// MARK: - CPC properties
@@ -57,6 +58,9 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
@IBOutlet var enterpriseBASICButton: NSPopUpButton!
@IBOutlet var enterpriseDOSButton: NSPopUpButton!
@IBOutlet var enterpriseExposePathButton: NSButton!
@IBOutlet var enterprisePathControl: NSPathControl!
// MARK: - Macintosh properties
@IBOutlet var macintoshModelTypeButton: NSPopUpButton!
@@ -137,6 +141,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
bbcDFSButton.state = standardUserDefaults.bool(forKey: "new.bbcDFS") ? .on : .off
bbcADFSButton.state = standardUserDefaults.bool(forKey: "new.bbcADFS") ? .on : .off
bbcSidewaysRAMButton.state = standardUserDefaults.bool(forKey: "new.bbcSidewaysRAM") ? .on : .off
bbcBeebSIDButton.state = standardUserDefaults.bool(forKey: "new.bbcBeebSID") ? .on : .off
bbcSecondProcessorButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.bbcSecondProcessor"))
// CPC settings
@@ -155,6 +160,9 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
enterpriseBASICButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.enterpriseBASICVersion"))
enterpriseDOSButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.enterpriseDOS"))
enterpriseExposePathButton.state = standardUserDefaults.bool(forKey: "new.enterpriseExposeLocalPath") ? .on : .off
establishPathControl(enterprisePathControl, userDefaultsKey: "new.enterpriseExposedLocalPath")
// Macintosh settings
macintoshModelTypeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.macintoshModel"))
@@ -217,6 +225,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
standardUserDefaults.set(bbcDFSButton.state == .on, forKey: "new.bbcDFS")
standardUserDefaults.set(bbcADFSButton.state == .on, forKey: "new.bbcADFS")
standardUserDefaults.set(bbcSidewaysRAMButton.state == .on, forKey: "new.bbcSidewaysRAM")
standardUserDefaults.set(bbcBeebSIDButton.state == .on, forKey: "new.bbcBeebSID")
standardUserDefaults.set(bbcSecondProcessorButton.selectedTag(), forKey: "new.bbcSecondProcessor")
// CPC settings
@@ -235,6 +244,9 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
standardUserDefaults.set(enterpriseBASICButton.selectedTag(), forKey: "new.enterpriseBASICVersion")
standardUserDefaults.set(enterpriseDOSButton.selectedTag(), forKey: "new.enterpriseDOS")
standardUserDefaults.set(enterpriseExposePathButton.state == .on, forKey: "new.enterpriseExposeLocalPath")
storePathControl(enterprisePathControl, userDefaultsKey: "new.enterpriseExposedLocalPath")
// Macintosh settings
standardUserDefaults.set(macintoshModelTypeButton.selectedTag(), forKey: "new.macintoshModel")
@@ -360,6 +372,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
bbcMicroDFS: bbcDFSButton.state == .on,
adfs: bbcADFSButton.state == .on,
sidewaysRAM: bbcSidewaysRAMButton.state == .on,
beebSID: bbcBeebSIDButton.state == .on,
secondProcessor: secondProcessor)
case "c16plus4":
@@ -426,7 +439,8 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
speed: speed,
exosVersion: exos,
basicVersion: basic,
dos: dos
dos: dos,
exposedLocalPath: enterpriseExposePathButton.state == .on ? enterprisePathControl.url : nil
)
case "mac":
@@ -528,4 +542,38 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
default: return CSStaticAnalyser()
}
}
// MARK: - NSPathControlDelegate (and paths in general)
func pathControl(_ pathControl: NSPathControl, willDisplay openPanel: NSOpenPanel) {
openPanel.canChooseFiles = false
openPanel.canChooseDirectories = true
}
func pathControl(_ pathControl: NSPathControl, validateDrop info: any NSDraggingInfo) -> NSDragOperation {
// Accept only directories.
if let url = NSURL(from: info.draggingPasteboard) {
if url.hasDirectoryPath {
return NSDragOperation.link
}
}
return []
}
func establishPathControl(_ pathControl: NSPathControl, userDefaultsKey: String) {
pathControl.url = FileManager.default.homeDirectoryForCurrentUser
if let bookmarkData = UserDefaults.standard.data(forKey: userDefaultsKey) {
var isStale: Bool = false
if let url = try? URL(resolvingBookmarkData: bookmarkData, bookmarkDataIsStale: &isStale) {
enterprisePathControl.url = url
}
}
}
func storePathControl(_ pathControl: NSPathControl, userDefaultsKey: String) {
let url = pathControl.url
if let bookmarkData = try? url?.bookmarkData(options: [.withSecurityScope]) {
UserDefaults.standard.set(bookmarkData, forKey: userDefaultsKey)
}
}
}

View File

@@ -146,6 +146,7 @@ SOURCES += \
$$SRC/Storage/Disk/Encodings/MFM/*.cpp \
$$SRC/Storage/Disk/Parsers/*.cpp \
$$SRC/Storage/Disk/Track/*.cpp \
$$SRC/Storage/FileBundle/*.cpp \
$$SRC/Storage/MassStorage/*.cpp \
$$SRC/Storage/MassStorage/Encodings/*.cpp \
$$SRC/Storage/MassStorage/Formats/*.cpp \
@@ -303,6 +304,7 @@ HEADERS += \
$$SRC/Storage/Disk/Encodings/MFM/*.hpp \
$$SRC/Storage/Disk/Parsers/*.hpp \
$$SRC/Storage/Disk/Track/*.hpp \
$$SRC/Storage/FileBundle/*.hpp \
$$SRC/Storage/MassStorage/*.hpp \
$$SRC/Storage/MassStorage/Encodings/*.hpp \
$$SRC/Storage/MassStorage/Formats/*.hpp \

View File

@@ -134,6 +134,7 @@ SOURCES += glob.glob('../../Storage/Disk/Encodings/MFM/*.cpp')
SOURCES += glob.glob('../../Storage/Disk/Parsers/*.cpp')
SOURCES += glob.glob('../../Storage/Disk/Track/*.cpp')
SOURCES += glob.glob('../../Storage/Disk/Data/*.cpp')
SOURCES += glob.glob('../../Storage/FileBundle/*.cpp')
SOURCES += glob.glob('../../Storage/MassStorage/*.cpp')
SOURCES += glob.glob('../../Storage/MassStorage/Encodings/*.cpp')
SOURCES += glob.glob('../../Storage/MassStorage/Formats/*.cpp')

View File

@@ -71,9 +71,13 @@ void Controller::process_write_completed() {
// Provided for subclasses to override.
}
void Controller::is_writing_final_bit() {
// Provided for subclasses to override.
}
// MARK: - PLL control and delegate
void Controller::set_expected_bit_length(Time bit_length) {
void Controller::set_expected_bit_length(const Time bit_length) {
bit_length_ = bit_length;
bit_length_.simplify();
@@ -86,6 +90,10 @@ void Controller::set_expected_bit_length(Time bit_length) {
pll_.set_clocks_per_bit(clocks_per_bit);
}
Storage::Time Controller::expected_bit_length() {
return bit_length_;
}
void Controller::digital_phase_locked_loop_output_bit(int value) {
if(is_reading_) process_input_bit(value);
}
@@ -124,9 +132,9 @@ void Controller::set_drive(int index_mask) {
}
}
void Controller::begin_writing(bool clamp_to_index_hole) {
void Controller::begin_writing(const bool clamp_to_index_hole, const bool synthesise_initial_writing_events) {
is_reading_ = false;
get_drive().begin_writing(bit_length_, clamp_to_index_hole);
get_drive().begin_writing(bit_length_, clamp_to_index_hole, synthesise_initial_writing_events);
}
void Controller::end_writing() {

View File

@@ -40,6 +40,7 @@ protected:
Communicates to the PLL the expected length of a bit as a fraction of a second.
*/
void set_expected_bit_length(Time bit_length);
Time expected_bit_length();
/*!
Advances the drive by @c number_of_cycles cycles.
@@ -85,10 +86,16 @@ protected:
/*!
Should be implemented by subclasses if they implement writing; communicates that
all bits supplied to write_bit have now been written.
all bits supplied to `write_bit` have now been written.
*/
virtual void process_write_completed() override;
/*!
Can be implemented by subclasses that perform writing; indicates that the final
bit previously provided to the drive has started its output.
*/
virtual void is_writing_final_bit() override;
/*!
Puts the controller and the drive returned by get_drive() into write mode, supplying to
the drive the current bit length.
@@ -99,7 +106,7 @@ protected:
@param clamp_to_index_hole If @c true then writing will automatically be truncated by
the index hole. Writing will continue over the index hole otherwise.
*/
void begin_writing(bool clamp_to_index_hole);
void begin_writing(bool clamp_to_index_hole, bool synthesise_initial_writing_events);
/*!
Puts the drive returned by get_drive() out of write mode, and marks the controller

View File

@@ -10,10 +10,13 @@
#include <algorithm>
#include <cstring>
#include <set>
#include <sys/stat.h>
#include "Storage/Disk/Track/PCMTrack.hpp"
#include "Storage/Disk/Encodings/CommodoreGCR.hpp"
#include "Storage/Disk/Track/TrackSerialiser.hpp"
#include "Numeric/SizedInt.hpp"
using namespace Storage::Disk;
@@ -38,23 +41,42 @@ HeadPosition D64::maximum_head_position() const {
return HeadPosition(number_of_tracks_);
}
std::unique_ptr<Track> D64::track_at_position(const Track::Address address) const {
// Figure out where this track starts on the disk.
bool D64::is_read_only() const {
return file_.is_known_read_only();
}
bool D64::represents(const std::string &name) const {
return name == file_.name();
}
D64::TrackExtent D64::track_extent(const Track::Address address) const {
static constexpr int tracks_in_zone[] = {17, 7, 6, 10};
static constexpr int sectors_by_zone[] = {21, 19, 18, 17};
int offset_to_track = 0;
int tracks_to_traverse = address.position.as_int();
int zone_sizes[] = {17, 7, 6, 10};
int sectors_by_zone[] = {21, 19, 18, 17};
int zone = 0;
for(int current_zone = 0; current_zone < 4; current_zone++) {
int tracks_in_this_zone = std::min(tracks_to_traverse, zone_sizes[current_zone]);
offset_to_track += tracks_in_this_zone * sectors_by_zone[current_zone];
tracks_to_traverse -= tracks_in_this_zone;
if(tracks_in_this_zone == zone_sizes[current_zone]) zone++;
const int tracks = std::min(tracks_to_traverse, tracks_in_zone[current_zone]);
offset_to_track += tracks * sectors_by_zone[current_zone];
tracks_to_traverse -= tracks;
if(tracks == tracks_in_zone[current_zone]) {
++zone;
}
}
return TrackExtent {
.file_offset = offset_to_track * 256,
.zone = zone,
.number_of_sectors = sectors_by_zone[zone]
};
}
std::unique_ptr<Track> D64::track_at_position(const Track::Address address) const {
// Seek to start of data.
file_.seek(offset_to_track * 256, Whence::SET);
const auto extent = track_extent(address);
std::lock_guard lock_guard(file_.file_access_mutex());
file_.seek(extent.file_offset, Whence::SET);
// Build up a PCM sampling of the GCR version of this track.
@@ -78,17 +100,17 @@ std::unique_ptr<Track> D64::track_at_position(const Track::Address address) cons
//
// = 349 GCR bytes per sector
std::size_t track_bytes = 349 * size_t(sectors_by_zone[zone]);
std::size_t track_bytes = 349 * size_t(extent.number_of_sectors);
std::vector<uint8_t> data(track_bytes);
for(int sector = 0; sector < sectors_by_zone[zone]; sector++) {
uint8_t *sector_data = &data[size_t(sector) * 349];
for(int sector = 0; sector < extent.number_of_sectors; sector++) {
uint8_t *const sector_data = &data[size_t(sector) * 349];
sector_data[0] = sector_data[1] = sector_data[2] = 0xff;
uint8_t sector_number = uint8_t(sector); // Sectors count from 0.
uint8_t track_number = uint8_t(address.position.as_int() + 1); // Tracks count from 1.
const uint8_t sector_number = uint8_t(sector); // Sectors count from 0.
const uint8_t track_number = uint8_t(address.position.as_int() + 1); // Tracks count from 1.
uint8_t checksum = uint8_t(sector_number ^ track_number ^ disk_id_ ^ (disk_id_ >> 8));
uint8_t header_start[4] = {
const uint8_t header_start[4] = {
0x08, checksum, sector_number, track_number
};
Encodings::CommodoreGCR::encode_block(&sector_data[3], header_start);
@@ -99,7 +121,7 @@ std::unique_ptr<Track> D64::track_at_position(const Track::Address address) cons
Encodings::CommodoreGCR::encode_block(&sector_data[8], header_end);
// Pad out post-header parts.
uint8_t zeros[4] = {0, 0, 0, 0};
static constexpr uint8_t zeros[4] = {0, 0, 0, 0};
Encodings::CommodoreGCR::encode_block(&sector_data[13], zeros);
sector_data[18] = 0x52;
sector_data[19] = 0x94;
@@ -111,14 +133,15 @@ std::unique_ptr<Track> D64::track_at_position(const Track::Address address) cons
// Compute the latest checksum.
checksum = 0;
for(int c = 0; c < 256; c++)
for(int c = 0; c < 256; c++) {
checksum ^= source_data[c];
}
// Put in another sync.
sector_data[21] = sector_data[22] = sector_data[23] = 0xff;
// Now start writing in the actual data.
uint8_t start_of_data[4] = {
const uint8_t start_of_data[4] = {
0x07, source_data[0], source_data[1], source_data[2]
};
Encodings::CommodoreGCR::encode_block(&sector_data[24], start_of_data);
@@ -129,7 +152,7 @@ std::unique_ptr<Track> D64::track_at_position(const Track::Address address) cons
target_data_offset += 5;
source_data_offset += 4;
}
uint8_t end_of_data[4] = {
const uint8_t end_of_data[4] = {
source_data[255], checksum, 0, 0
};
Encodings::CommodoreGCR::encode_block(&sector_data[target_data_offset], end_of_data);
@@ -138,6 +161,103 @@ std::unique_ptr<Track> D64::track_at_position(const Track::Address address) cons
return std::make_unique<PCMTrack>(PCMSegment(data));
}
bool D64::represents(const std::string &name) const {
return name == file_.name();
void D64::set_tracks(const std::map<Track::Address, std::unique_ptr<Track>> &tracks) {
for(const auto &[address, track]: tracks) {
const auto extent = track_extent(address);
std::map<int, std::vector<uint8_t>> decoded;
// Get bit stream.
const auto serialisation =
Storage::Disk::track_serialisation(
*track,
Time(1, extent.number_of_sectors * 349 * 8) // This is relative to a normalised world where
// 1 unit of time = 1 track. So don't use
// length_of_a_bit_in_time_zone, which is relative to
// a wall clock.
);
// Decode sectors.
Numeric::SizedInt<10> shifter = 0;
int repeats = 2;
auto bit = serialisation.data.begin();
bool is_ended = false;
const auto shift = [&] {
shifter = uint16_t((shifter.get() << 1) | *bit);
++bit;
if(bit == serialisation.data.end()) {
bit = serialisation.data.begin();
--repeats;
is_ended |= !repeats;
}
};
const auto byte = [&] {
for(int c = 0; c < 9; c++) {
shift();
}
const auto result = Encodings::CommodoreGCR::decoding_from_dectet(shifter.get());
shift();
return uint8_t(result);
};
const auto block_type = [&] {
// Find synchronisation, then get first dectet after that.
while(!is_ended && shifter.get() != 0b11111'11111) {
shift();
}
while(!is_ended && shifter.get() == 0b11111'11111) {
shift();
}
// Type should be 8 for a header, 7 for some data.
return byte();
};
while(!is_ended && decoded.size() != size_t(extent.number_of_sectors)) {
// Find a header.
const auto header_start = block_type();
if(header_start != 0x8) {
continue;
}
const auto checksum = byte();
const auto sector_id = byte();
const auto track_id = byte();
const auto disk_id1 = byte();
const auto disk_id2 = byte();
if(checksum != (sector_id ^ track_id ^ disk_id1 ^ disk_id2)) {
continue;
}
if(sector_id >= extent.number_of_sectors) {
continue;
}
// Skip to data.
const auto data_start = block_type();
if(data_start != 0x7) {
continue;
}
// Copy into place if not yet present.
uint8_t data_checksum = 0;
std::vector<uint8_t> sector_contents(256);
for(size_t c = 0; c < 256; c++) {
const uint8_t next = byte();
data_checksum ^= next;
sector_contents[c] = next;
}
if(byte() != data_checksum) {
continue;
}
decoded.emplace(sector_id, std::move(sector_contents));
}
// Write.
std::lock_guard lock_guard(file_.file_access_mutex());
for(auto &[sector, contents]: decoded) {
file_.seek(extent.file_offset + sector * 256, Whence::SET);
file_.write(contents);
}
}
}

View File

@@ -28,12 +28,21 @@ public:
HeadPosition maximum_head_position() const;
std::unique_ptr<Track> track_at_position(Track::Address) const;
bool is_read_only() const;
void set_tracks(const std::map<Track::Address, std::unique_ptr<Track>> &);
bool represents(const std::string &) const;
private:
mutable Storage::FileHolder file_;
int number_of_tracks_;
uint16_t disk_id_;
struct TrackExtent {
long file_offset;
int zone;
int number_of_sectors;
};
TrackExtent track_extent(Track::Address) const;
};
}

View File

@@ -16,13 +16,19 @@
using namespace Storage::Disk;
Drive::Drive(int input_clock_rate, int revolutions_per_minute, int number_of_heads, ReadyType rdy_type):
Drive::Drive(
const int input_clock_rate,
const int revolutions_per_minute,
const int number_of_heads,
const ReadyType rdy_type
) :
Storage::TimedEventLoop(input_clock_rate),
available_heads_(number_of_heads),
ready_type_(rdy_type) {
set_rotation_speed(revolutions_per_minute);
const auto seed = std::default_random_engine::result_type(std::chrono::system_clock::now().time_since_epoch().count());
const auto seed =
std::default_random_engine::result_type(std::chrono::system_clock::now().time_since_epoch().count());
std::default_random_engine randomiser(seed);
// Get at least 64 bits of random information; rounding is likey to give this a slight bias.
@@ -34,16 +40,21 @@ Drive::Drive(int input_clock_rate, int revolutions_per_minute, int number_of_hea
}
}
Drive::Drive(int input_clock_rate, int number_of_heads, ReadyType rdy_type) : Drive(input_clock_rate, 300, number_of_heads, rdy_type) {}
Drive::Drive(
const int input_clock_rate,
const int number_of_heads,
const ReadyType rdy_type
) : Drive(input_clock_rate, 300, number_of_heads, rdy_type) {}
void Drive::set_rotation_speed(float revolutions_per_minute) {
void Drive::set_rotation_speed(const float revolutions_per_minute) {
// Rationalise the supplied speed so that cycles_per_revolution_ is exact.
cycles_per_revolution_ = int(0.5f + float(get_input_clock_rate()) * 60.0f / revolutions_per_minute);
// From there derive the appropriate rotational multiplier and possibly update the
// count of cycles since the index hole proportionally.
const float new_rotational_multiplier = float(cycles_per_revolution_) / float(get_input_clock_rate());
cycles_since_index_hole_ = Cycles::IntType(float(cycles_since_index_hole_) * new_rotational_multiplier / rotational_multiplier_);
cycles_since_index_hole_ =
Cycles::IntType(float(cycles_since_index_hole_) * new_rotational_multiplier / rotational_multiplier_);
rotational_multiplier_ = new_rotational_multiplier;
cycles_since_index_hole_ %= cycles_per_revolution_;
}
@@ -75,14 +86,16 @@ bool Drive::has_disk() const {
}
ClockingHint::Preference Drive::preferred_clocking() const {
return (!has_disk_ || (time_until_motor_transition == Cycles(0) && !disk_is_rotating_)) ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime;
return (
!has_disk_ || (time_until_motor_transition == Cycles(0) && !disk_is_rotating_)
) ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime;
}
bool Drive::get_is_track_zero() const {
return head_position_ == HeadPosition(0);
}
void Drive::step(HeadPosition offset) {
void Drive::step(const HeadPosition offset) {
if(offset == HeadPosition(0)) {
return;
}
@@ -109,7 +122,7 @@ void Drive::step(HeadPosition offset) {
did_step(head_position_);
}
Track *Drive::step_to(HeadPosition offset) {
Track *Drive::step_to(const HeadPosition offset) {
HeadPosition old_head_position = head_position_;
head_position_ = std::max(offset, HeadPosition(0));
@@ -160,7 +173,7 @@ bool Drive::get_is_ready() const {
return is_ready_;
}
void Drive::set_motor_on(bool motor_is_on) {
void Drive::set_motor_on(const bool motor_is_on) {
// Do nothing if the input hasn't changed.
if(motor_input_is_on_ == motor_is_on) return;
motor_input_is_on_ = motor_is_on;
@@ -195,7 +208,7 @@ bool Drive::get_index_pulse() const {
return index_pulse_remaining_ > Cycles(0);
}
void Drive::set_event_delegate(Storage::Disk::Drive::EventDelegate *delegate) {
void Drive::set_event_delegate(Storage::Disk::Drive::EventDelegate *const delegate) {
event_delegate_ = delegate;
}
@@ -237,13 +250,21 @@ void Drive::run_for(const Cycles cycles) {
if(cycles_until_bits_written_ > zero) {
Storage::Time cycles_to_run_for_time(static_cast<int>(cycles_to_run_for));
if(cycles_until_bits_written_ <= cycles_to_run_for_time) {
if(event_delegate_) event_delegate_->process_write_completed();
if(cycles_until_bits_written_ <= cycles_to_run_for_time)
cycles_until_bits_written_.set_zero();
else
cycles_until_bits_written_ -= cycles_to_run_for_time;
if(event_delegate_) {
event_delegate_->process_write_completed();
}
} else {
const auto previous_cycles = cycles_until_bits_written_;
cycles_until_bits_written_ -= cycles_to_run_for_time;
if(
previous_cycles >= cycles_per_bit_ &&
cycles_until_bits_written_ < cycles_per_bit_ &&
event_delegate_
) {
event_delegate_->is_writing_final_bit();
}
}
}
}
@@ -257,7 +278,7 @@ void Drive::run_for(const Cycles cycles) {
// MARK: - Track timed event loop
void Drive::get_next_event(float duration_already_passed) {
void Drive::get_next_event(const float duration_already_passed) {
/*
Quick word on random-bit generation logic below; it seeks to obey the following logic:
if there is a gap of 15µs between recorded bits, start generating flux transitions
@@ -387,7 +408,11 @@ void Drive::invalidate_track() {
// MARK: - Writing
void Drive::begin_writing(Time bit_length, bool clamp_to_index_hole) {
void Drive::begin_writing(
const Time bit_length,
const bool clamp_to_index_hole,
const bool synthesise_initial_writing_events
) {
// Do nothing if already writing.
// TODO: cope properly if there's no disk to write to.
if(!is_reading_ || !disk_) return;
@@ -407,10 +432,15 @@ void Drive::begin_writing(Time bit_length, bool clamp_to_index_hole) {
write_segment_.length_of_a_bit = bit_length / Time(rotational_multiplier_);
write_segment_.data.clear();
cycles_until_bits_written_.set_zero();
write_start_time_ = Time(get_time_into_track());
if(synthesise_initial_writing_events) {
write_start_time_ += bit_length;
cycles_until_bits_written_ += cycles_per_bit_;
}
}
void Drive::write_bit(bool value) {
void Drive::write_bit(const bool value) {
write_segment_.data.push_back(value);
cycles_until_bits_written_ += cycles_per_bit_;
}
@@ -444,7 +474,7 @@ bool Drive::is_writing() const {
return !is_reading_;
}
void Drive::set_disk_is_rotating(bool is_rotating) {
void Drive::set_disk_is_rotating(const bool is_rotating) {
disk_is_rotating_ = is_rotating;
if(observer_) {
@@ -464,7 +494,11 @@ void Drive::set_disk_is_rotating(bool is_rotating) {
update_clocking_observer();
}
void Drive::set_activity_observer(Activity::Observer *observer, const std::string &name, bool add_motor_led) {
void Drive::set_activity_observer(
Activity::Observer *const observer,
const std::string &name,
const bool add_motor_led
) {
observer_ = observer;
announce_motor_led_ = add_motor_led;
if(observer) {

View File

@@ -36,13 +36,7 @@ public:
Drive(int input_clock_rate, int number_of_heads, ReadyType rdy_type = ReadyType::ShugartRDY);
virtual ~Drive();
// TODO: Disallow copying.
//
// GCC 10 has an issue with the way the DiskII constructs its drive array if these are both
// deleted, despite not using the copy constructor.
//
// This seems to be fixed in GCC 11, so reenable this delete when possible.
// Drive(const Drive &) = delete;
Drive(const Drive &) = delete;
void operator=(const Drive &) = delete;
/*!
@@ -74,7 +68,7 @@ public:
/*!
Sets the current read head.
*/
void set_head(int head);
void set_head(int);
/*!
Gets the head count for this disk.
@@ -114,13 +108,16 @@ public:
@param clamp_to_index_hole If @c true then writing will automatically be truncated by
the index hole. Writing will continue over the index hole otherwise.
@param synthesise_initial_writing_events if @c true then an @c is_writing_final_bit() /
@c process_write_completed() pair will follow without any data having been written.
*/
void begin_writing(Time bit_length, bool clamp_to_index_hole);
void begin_writing(Time bit_length, bool clamp_to_index_hole, bool synthesise_initial_writing_events);
/*!
Writes the bit @c value as the next in the PCM stream initiated by @c begin_writing.
*/
void write_bit(bool value);
void write_bit(bool);
/*!
Ends write mode, switching back to read mode. The drive will stop overwriting events.
@@ -149,7 +146,7 @@ public:
*/
struct EventDelegate {
/// Informs the delegate that @c event has been reached.
virtual void process_event(const Event &event) = 0;
virtual void process_event(const Event &) = 0;
/*!
If the drive is in write mode, announces that all queued bits have now been written.
@@ -157,8 +154,13 @@ public:
*/
virtual void process_write_completed() {}
/*!
When in write mode, indicates that output of the final bit has begun.
*/
virtual void is_writing_final_bit() {}
/// Informs the delegate of the passing of @c cycles.
virtual void advance([[maybe_unused]] Cycles cycles) {}
virtual void advance(Cycles) {}
};
/// Sets the current event delegate.

View File

@@ -12,7 +12,8 @@
using namespace Storage;
Time Storage::Encodings::CommodoreGCR::length_of_a_bit_in_time_zone(const unsigned int time_zone) {
// the speed zone divides a 4Mhz clock by 13, 14, 15 or 16, with higher-numbered zones being faster (i.e. each bit taking less time)
// The speed zone divides a 4Mhz clock by 13, 14, 15 or 16, with higher-numbered zones being
// faster (i.e. each bit taking less time).
return Time(16 - time_zone, 4000000u);
}

View File

@@ -64,7 +64,7 @@ public:
HeadPosition position;
constexpr auto operator <=>(const Address&) const = default;
constexpr Address(int head, HeadPosition position) : head(head), position(position) {}
constexpr Address(const int head, const HeadPosition position) noexcept : head(head), position(position) {}
};
/*!

View File

@@ -12,7 +12,7 @@
// TODO: if this is a PCMTrack with only one segment and that segment's bit rate is within tolerance,
// just return a copy of that segment.
Storage::Disk::PCMSegment Storage::Disk::track_serialisation(const Track &track, Time length_of_a_bit) {
Storage::Disk::PCMSegment Storage::Disk::track_serialisation(const Track &track, const Time length_of_a_bit) {
unsigned int history_size = 16;
std::unique_ptr<Track> track_copy(track.clone());

View File

@@ -0,0 +1,67 @@
//
// FileBundle.cpp
// Clock Signal
//
// Created by Thomas Harte on 19/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#include "FileBundle.hpp"
#include <cstdio>
#include <sys/stat.h>
using namespace Storage::FileBundle;
LocalFSFileBundle::LocalFSFileBundle(const std::string &to_contain) {
struct stat stats;
stat(to_contain.c_str(), &stats);
if(S_ISDIR(stats.st_mode)) {
set_base_path(to_contain);
} else {
const auto last_separator = to_contain.find_last_of("/\\");
if(last_separator == std::string::npos) {
key_file_ = to_contain;
} else {
base_path_ = to_contain.substr(0, last_separator + 1);
key_file_ = to_contain.substr(last_separator + 1);
}
}
}
std::optional<std::string> LocalFSFileBundle::key_file() const {
if(key_file_.empty()) {
return std::nullopt;
}
return key_file_;
}
std::optional<std::string> LocalFSFileBundle::base_path() const {
return base_path_;
}
void LocalFSFileBundle::set_base_path(const std::string &path) {
base_path_ = path;
if(base_path_.back() != '/') {
base_path_ += '/';
}
}
void LocalFSFileBundle::set_permission_delegate(PermissionDelegate *const delegate) {
permission_delegate_ = delegate;
}
Storage::FileHolder LocalFSFileBundle::open(const std::string &name, const Storage::FileMode mode) {
if(permission_delegate_) {
permission_delegate_->validate_open(*this, base_path_ + name, mode);
}
return Storage::FileHolder(base_path_ + name, mode);
}
bool LocalFSFileBundle::erase(const std::string &name) {
if(permission_delegate_) {
permission_delegate_->validate_erase(*this, base_path_ + name);
}
return !remove((base_path_ + name).c_str());
}

View File

@@ -0,0 +1,64 @@
//
// FileBundle.hpp
// Clock Signal
//
// Created by Thomas Harte on 19/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Storage/FileHolder.hpp"
#include <optional>
#include <string>
namespace Storage::FileBundle {
/*!
A File Bundle is a collection of individual files, abstracted from whatever media they might
be one.
Initial motivation is allowing some machines direct local filesystem access. An attempt has
been made to draft this in such a way as to allow it to do things like expose ZIP files as
bundles in the future.
*/
struct FileBundle {
virtual ~FileBundle() {}
struct PermissionDelegate {
virtual void validate_open(FileBundle &, const std::string &, FileMode) = 0;
virtual void validate_erase(FileBundle &, const std::string &) = 0;
};
virtual std::optional<std::string> key_file() const = 0;
virtual FileHolder open(const std::string &, FileMode) = 0;
virtual bool erase(const std::string &) = 0;
virtual std::optional<std::string> base_path() const { return std::nullopt; }
virtual void set_base_path(const std::string &) {}
virtual void set_permission_delegate(PermissionDelegate *) {}
virtual void set_case_insensitive(bool) {}
};
struct LocalFSFileBundle: public FileBundle {
LocalFSFileBundle(const std::string &to_contain);
std::optional<std::string> key_file() const override;
FileHolder open(const std::string &, FileMode) override;
bool erase(const std::string &) override;
std::optional<std::string> base_path() const override;
void set_base_path(const std::string &) override;
void set_permission_delegate(PermissionDelegate *) override;
// TODO: implement case insensitive matching.
private:
std::string key_file_;
std::string base_path_;
PermissionDelegate *permission_delegate_ = nullptr;
};
};

View File

@@ -13,6 +13,12 @@
using namespace Storage;
FileHolder::FileHolder(FileHolder &&rhs) {
file_ = rhs.file_;
rhs.file_ = nullptr;
// TODO: this leaves the RHS in an invalid state, which isn't appropriate for move semantics.
}
FileHolder::~FileHolder() {
if(file_) std::fclose(file_);
}
@@ -45,8 +51,8 @@ uint8_t FileHolder::get() {
return uint8_t(std::fgetc(file_));
}
void FileHolder::put(const uint8_t value) {
std::fputc(value, file_);
bool FileHolder::put(const uint8_t value) {
return std::fputc(value, file_) == value;
}
void FileHolder::putn(std::size_t repeats, const uint8_t value) {
@@ -71,9 +77,9 @@ std::size_t FileHolder::write(const uint8_t *buffer, const std::size_t size) {
return std::fwrite(buffer, 1, size, file_);
}
void FileHolder::seek(const long offset, const Whence whence) {
[[maybe_unused]] const auto result = std::fseek(file_, offset, int(whence));
assert(!result);
bool FileHolder::seek(const long offset, const Whence whence) {
const auto result = std::fseek(file_, offset, int(whence));
return !result;
}
long FileHolder::tell() const {

View File

@@ -56,9 +56,10 @@ public:
Rewrite opens the file for rewriting; none of the original content is preserved; whatever
the caller outputs will replace the existing file.
@throws ErrorCantOpen if the file cannot be opened.
@throws Error::CantOpen if the file cannot be opened.
*/
FileHolder(const std::string &file_name, FileMode ideal_mode = FileMode::ReadWrite);
FileHolder(FileHolder &&);
/*!
Writes @c value using successive @c puts, in little endian order.
@@ -116,8 +117,11 @@ public:
/*! Reads a single byte from @c file. */
uint8_t get();
/*! Writes a single byte from @c file. */
void put(uint8_t);
/*!
Writes a single byte from @c file.
@returns @c true on success; @c false on failure.
*/
bool put(uint8_t);
/*! Writes @c value a total of @c repeats times. */
void putn(std::size_t repeats, uint8_t value);
@@ -140,7 +144,7 @@ public:
std::size_t write(const uint8_t *, std::size_t);
/*! Moves @c bytes from the anchor indicated by @c whence: SEEK_SET, SEEK_CUR or SEEK_END. */
void seek(long offset, Whence);
bool seek(long offset, Whence);
/*! @returns The current cursor position within this file. */
long tell() const;

View File

@@ -94,7 +94,7 @@ void TapePlayer::set_tape(std::shared_ptr<Storage::Tape::Tape> tape, TargetPlatf
}
bool TapePlayer::is_at_end() const {
return serialiser_->is_at_end();
return !serialiser_ || serialiser_->is_at_end();
}
TapeSerialiser *TapePlayer::serialiser() {

View File

@@ -130,6 +130,7 @@ set(CLK_SOURCES
Machines/Enterprise/Dave.cpp
Machines/Enterprise/EXDos.cpp
Machines/Enterprise/Enterprise.cpp
Machines/Enterprise/HostFSHandler.cpp
Machines/Enterprise/Keyboard.cpp
Machines/Enterprise/Nick.cpp
Machines/KeyboardMachine.cpp
@@ -224,6 +225,7 @@ set(CLK_SOURCES
Storage/Disk/Track/PCMTrack.cpp
Storage/Disk/Track/TrackSerialiser.cpp
Storage/Disk/Track/UnformattedTrack.cpp
Storage/FileBundle/FileBundle.cpp
Storage/FileHolder.cpp
Storage/MassStorage/Encodings/MacintoshVolume.cpp
Storage/MassStorage/Formats/DAT.cpp