mirror of
https://github.com/TomHarte/CLK.git
synced 2025-11-23 21:17:42 +00:00
Compare commits
68 Commits
QueueDeleg
...
macOSPermi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74736d9723 | ||
|
|
5c20fcefc4 | ||
|
|
e9ba1c4ede | ||
|
|
6767ddde4c | ||
|
|
56a326d7db | ||
|
|
d340a01513 | ||
|
|
2dd2b279c8 | ||
|
|
88c124aa9f | ||
|
|
e2ef3226af | ||
|
|
507b81a8a4 | ||
|
|
31d7639761 | ||
|
|
10847b3e0b | ||
|
|
7b513a95a1 | ||
|
|
411b96128c | ||
|
|
5ef5478861 | ||
|
|
50adbaefc8 | ||
|
|
424d57c2c1 | ||
|
|
76ed9d1703 | ||
|
|
c4bab00c6d | ||
|
|
ac9fc15981 | ||
|
|
1b977cae64 | ||
|
|
0cd1921971 | ||
|
|
c193315e17 | ||
|
|
364996021e | ||
|
|
f287c80e39 | ||
|
|
f4a9a64c93 | ||
|
|
c464ffaeac | ||
|
|
822aa4155d | ||
|
|
3d192e22f2 | ||
|
|
6e8e2b6201 | ||
|
|
16e4144409 | ||
|
|
6dacc50163 | ||
|
|
6d71ad9bcc | ||
|
|
fc5d93f9cc | ||
|
|
3196840b05 | ||
|
|
51eea4dea3 | ||
|
|
314154e9fd | ||
|
|
4a93264dc5 | ||
|
|
39a96a7f73 | ||
|
|
9ee7425627 | ||
|
|
923fdd42ec | ||
|
|
50cd28f882 | ||
|
|
7bc865a2e0 | ||
|
|
1cf3c77ae9 | ||
|
|
d3cda5d878 | ||
|
|
4f09f38f2e | ||
|
|
4980caee1b | ||
|
|
ae89c66b17 | ||
|
|
bb7059a9e1 | ||
|
|
05de67ba76 | ||
|
|
25bf7df4d1 | ||
|
|
e44cbcc1d5 | ||
|
|
c876bcb849 | ||
|
|
febff84421 | ||
|
|
1ca261986e | ||
|
|
71e319a815 | ||
|
|
a67e222c35 | ||
|
|
e173a93b57 | ||
|
|
3dbf62ca08 | ||
|
|
bb5239e553 | ||
|
|
faec5c3f84 | ||
|
|
9c359627f3 | ||
|
|
2a0208c554 | ||
|
|
f513edc006 | ||
|
|
9e39be282b | ||
|
|
d628f75244 | ||
|
|
cd09b5d356 | ||
|
|
bb2cf0170d |
@@ -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(
|
||||
}
|
||||
}
|
||||
|
||||
targets.push_back(std::unique_ptr<Analyser::Static::Target>(target));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
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
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
159
Machines/Enterprise/EXOSCodes.hpp
Normal file
159
Machines/Enterprise/EXOSCodes.hpp
Normal 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 1–11.
|
||||
//
|
||||
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
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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,13 +615,23 @@ 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_;
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
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};
|
||||
@@ -595,20 +650,29 @@ private:
|
||||
template <size_t slot> void page(const uint8_t offset) {
|
||||
pages_[slot] = offset;
|
||||
|
||||
if constexpr (slot == 3) {
|
||||
test_host_fs_traps_ = false;
|
||||
}
|
||||
|
||||
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;
|
||||
if(page_rom<slot>(offset, 64, host_fs_rom_)) {
|
||||
if constexpr (slot == 3) {
|
||||
test_host_fs_traps_ = true;
|
||||
}
|
||||
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);
|
||||
page<slot>(pointer, pointer);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -621,7 +685,6 @@ private:
|
||||
}
|
||||
|
||||
// MARK: - Memory Timing
|
||||
|
||||
// The wait mode affects all memory accesses _outside of the video area_.
|
||||
enum class WaitMode {
|
||||
None,
|
||||
@@ -631,6 +694,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 +770,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 +809,69 @@ 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;
|
||||
|
||||
uint8_t hostfs_read(const uint16_t address) override {
|
||||
if(read_pointers_[address >> 14]) {
|
||||
return read_pointers_[address >> 14][address];
|
||||
} else {
|
||||
return 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t &user_ram(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 page_id = address >> 14;
|
||||
const uint8_t page = read_pointers_[0xbffc >> 14] ? read_pointers_[0xbffc >> 14][0xbffc + page_id] : 0xff;
|
||||
const auto offset = address & 0x3fff;
|
||||
return ram_segment(page)[offset];
|
||||
}
|
||||
|
||||
uint8_t hostfs_user_read(const uint16_t address) override {
|
||||
return user_ram(address);
|
||||
}
|
||||
|
||||
void hostfs_user_write(const uint16_t address, const uint8_t value) override {
|
||||
user_ram(address) = value;
|
||||
}
|
||||
|
||||
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 +879,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();
|
||||
|
||||
1
Machines/Enterprise/HostFS/compile.sh
Executable file
1
Machines/Enterprise/HostFS/compile.sh
Executable file
@@ -0,0 +1 @@
|
||||
pyz80.py --obj=hostfs.rom hostfs.z80s && xxd -i hostfs.rom
|
||||
215
Machines/Enterprise/HostFS/hostfs.z80s
Normal file
215
Machines/Enterprise/HostFS/hostfs.z80s
Normal 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
|
||||
262
Machines/Enterprise/HostFSHandler.cpp
Normal file
262
Machines/Enterprise/HostFSHandler.cpp
Normal file
@@ -0,0 +1,262 @@
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
52
Machines/Enterprise/HostFSHandler.hpp
Normal file
52
Machines/Enterprise/HostFSHandler.hpp
Normal 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_;
|
||||
};
|
||||
|
||||
};
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -171,15 +181,7 @@ typedef int Kilobytes;
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@@ -30,6 +30,113 @@
|
||||
|
||||
#import "Clock_Signal-Swift.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct PermissionDelegate: public Storage::FileBundle::FileBundle::PermissionDelegate {
|
||||
void validate_open(const std::string &, Storage::FileMode) {
|
||||
// TODO.
|
||||
// // (1) Does this file bundle have a base path?
|
||||
// const auto path = bundle->base_path();
|
||||
// if(!path.has_value()) {
|
||||
// continue;
|
||||
// }
|
||||
// NSString *pathName = [NSString stringWithUTF8String:path->c_str()];
|
||||
//
|
||||
// // (2) Can everything in that base path already be freely read?
|
||||
// NSError *error;
|
||||
// NSArray<NSString *> *allFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:pathName error:&error];
|
||||
// BOOL hasFullAccess = YES;
|
||||
// for(NSString *file in allFiles) {
|
||||
// FILE *pilot = fopen([[pathName stringByAppendingPathComponent:file] UTF8String], "rb");
|
||||
// if(!pilot) {
|
||||
// hasFullAccess = NO;
|
||||
// break;
|
||||
// }
|
||||
// fclose(pilot);
|
||||
// }
|
||||
// if(hasFullAccess) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// // (3) Ask the user for permission.
|
||||
// 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:[NSURL fileURLWithPath:pathName isDirectory:YES]];
|
||||
//
|
||||
// request.accessoryView = [NSTextField labelWithString:
|
||||
// @"Clock Signal is sandboxed; it cannot access any of your files without explicit permission.\n"
|
||||
// @"The type of program you are loading might require access to other files in its directory, which this "
|
||||
// @"application does not currently have permission to do.\n"
|
||||
// @"Please select 'Grant Permission' to give it permission to do so."
|
||||
// ];
|
||||
// request.accessoryViewDisclosed = YES;
|
||||
// // TODO: use delegate further to shepherd user.
|
||||
//
|
||||
// const auto response = [request runModal];
|
||||
// if(response != NSModalResponseOK) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// // Possibly substitute the base path, in case the one returned
|
||||
// // is an indirection out of the sandbox.
|
||||
// if(![request.URL isEqual:[NSURL fileURLWithPath:pathName]]) {
|
||||
// NSLog(@"Need to substitute: %@", request.URL);
|
||||
// }
|
||||
//
|
||||
// // TODO: bookmarkDataWithOptions on the URL, and store that somewhere for
|
||||
// // later retrieval. Then try that again if the same directory presents itself.
|
||||
}
|
||||
void validate_erase(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;
|
||||
}
|
||||
@@ -438,26 +545,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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(§or_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(§or_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(§or_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(§or_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(§or_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
cycles_until_bits_written_.set_zero();
|
||||
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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
};
|
||||
|
||||
/*!
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
51
Storage/FileBundle/FileBundle.cpp
Normal file
51
Storage/FileBundle/FileBundle.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// FileBundle.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 19/11/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "FileBundle.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
using namespace Storage::FileBundle;
|
||||
|
||||
LocalFSFileBundle::LocalFSFileBundle(const std::string &to_contain) {
|
||||
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() {
|
||||
return key_file_;
|
||||
}
|
||||
|
||||
void LocalFSFileBundle::set_permission_delegate(PermissionDelegate *const delegate) {
|
||||
permission_delegate_ = delegate;
|
||||
}
|
||||
|
||||
Storage::FileHolder LocalFSFileBundle::open(const std::string &name, const Storage::FileMode mode) {
|
||||
const auto full_name = base_path_ + name;
|
||||
if(permission_delegate_) {
|
||||
permission_delegate_->validate_open(full_name, mode);
|
||||
}
|
||||
return Storage::FileHolder(full_name, mode);
|
||||
}
|
||||
|
||||
bool LocalFSFileBundle::erase(const std::string &name) {
|
||||
const auto full_name = base_path_ + name;
|
||||
if(permission_delegate_) {
|
||||
permission_delegate_->validate_erase(full_name);
|
||||
}
|
||||
return !remove((base_path_ + name).c_str());
|
||||
}
|
||||
|
||||
std::optional<std::string> LocalFSFileBundle::base_path() {
|
||||
return base_path_;
|
||||
}
|
||||
56
Storage/FileBundle/FileBundle.hpp
Normal file
56
Storage/FileBundle/FileBundle.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// 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 {
|
||||
struct PermissionDelegate {
|
||||
virtual void validate_open(const std::string &, FileMode) = 0;
|
||||
virtual void validate_erase(const std::string &) = 0;
|
||||
};
|
||||
|
||||
virtual std::optional<std::string> key_file() = 0;
|
||||
virtual FileHolder open(const std::string &, FileMode) = 0;
|
||||
virtual bool erase(const std::string &) = 0;
|
||||
|
||||
virtual std::optional<std::string> base_path() = 0;
|
||||
virtual void set_permission_delegate(PermissionDelegate *) = 0;
|
||||
};
|
||||
|
||||
|
||||
struct LocalFSFileBundle: public FileBundle {
|
||||
LocalFSFileBundle(const std::string &to_contain);
|
||||
|
||||
std::optional<std::string> key_file() override;
|
||||
FileHolder open(const std::string &, FileMode) override;
|
||||
bool erase(const std::string &) override;
|
||||
|
||||
std::optional<std::string> base_path() override;
|
||||
void set_permission_delegate(PermissionDelegate *) override;
|
||||
|
||||
private:
|
||||
std::string key_file_;
|
||||
std::string base_path_;
|
||||
PermissionDelegate *permission_delegate_ = nullptr;
|
||||
};
|
||||
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user