1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-11-23 21:17:42 +00:00

Compare commits

...

116 Commits

Author SHA1 Message Date
Thomas Harte
d340a01513 Merge pull request #1643 from TomHarte/EXOSDirect
Enterprise: support .BAS and .COM and corresponding host filing system access.
2025-11-22 12:38:33 -05:00
Thomas Harte
2dd2b279c8 Note reason for that particular way of marking code. 2025-11-22 09:49:06 -05:00
Thomas Harte
88c124aa9f Implement destroy channel, treat it as the fallback for create channel. 2025-11-22 09:46:02 -05:00
Thomas Harte
e2ef3226af Implement destroy channel. 2025-11-22 09:39:05 -05:00
Thomas Harte
507b81a8a4 Implement SetChannelStatus. 2025-11-22 09:16:53 -05:00
Thomas Harte
31d7639761 Force user/native space selection into call sites. 2025-11-21 22:53:58 -05:00
Thomas Harte
10847b3e0b Fill in everything except SetChannelStatus. 2025-11-21 22:44:49 -05:00
Thomas Harte
7b513a95a1 Restructure, separating create from open. 2025-11-21 22:24:31 -05:00
Thomas Harte
411b96128c Further address type conversions. 2025-11-21 22:07:30 -05:00
Thomas Harte
5ef5478861 Resolve signedness warning. 2025-11-21 21:54:43 -05:00
Thomas Harte
50adbaefc8 Support BAS files, use file name from guest. 2025-11-21 21:53:53 -05:00
Thomas Harte
424d57c2c1 Fix executable name. 2025-11-21 21:30:02 -05:00
Thomas Harte
76ed9d1703 Add FileMode to key file loader. 2025-11-21 21:11:27 -05:00
Thomas Harte
c4bab00c6d Set file: as default filing system when in use. 2025-11-21 21:06:00 -05:00
Thomas Harte
ac9fc15981 Banish magic constant. 2025-11-21 17:03:28 -05:00
Thomas Harte
1b977cae64 Add TODO. 2025-11-21 15:56:44 -05:00
Thomas Harte
0cd1921971 Resolve addressing confusion. 2025-11-21 15:54:43 -05:00
Thomas Harte
c193315e17 Further consolidate for style. 2025-11-21 13:28:45 -05:00
Thomas Harte
364996021e Deal correctly with BC = 0. 2025-11-21 13:18:42 -05:00
Thomas Harte
f287c80e39 Pass on EXOS error code if received. 2025-11-21 13:14:52 -05:00
Thomas Harte
f4a9a64c93 Slightly rearrange. 2025-11-21 13:11:05 -05:00
Thomas Harte
c464ffaeac Attempt read block. 2025-11-21 13:08:52 -05:00
Thomas Harte
822aa4155d The read character is supposed to go into B. 2025-11-21 13:02:13 -05:00
Thomas Harte
3d192e22f2 It seems error codes changed at some point. 2025-11-21 12:58:54 -05:00
Thomas Harte
6e8e2b6201 Call EXOS for buffer allocation. 2025-11-21 12:32:06 -05:00
Thomas Harte
16e4144409 Switch to local ROM. 2025-11-20 22:30:15 -05:00
Thomas Harte
6dacc50163 Add dispatch table and hooks. 2025-11-20 21:50:57 -05:00
Thomas Harte
6d71ad9bcc I think this is code, not a pointer. 2025-11-20 20:11:20 -05:00
Thomas Harte
fc5d93f9cc Focus on writing an in-machine ROM. 2025-11-20 20:08:00 -05:00
Thomas Harte
3196840b05 Avoiding losing channel number; implement EOF check. 2025-11-20 17:26:44 -05:00
Thomas Harte
51eea4dea3 Attempt read character. 2025-11-20 17:16:30 -05:00
Thomas Harte
314154e9fd Implement open/create and close, of the key file. 2025-11-20 17:12:31 -05:00
Thomas Harte
4a93264dc5 Add move semantics. 2025-11-20 17:12:15 -05:00
Thomas Harte
39a96a7f73 Proceed to realisation that I'm probably looking in the wrong RAM. 2025-11-20 16:15:04 -05:00
Thomas Harte
9ee7425627 Add missing include for std::shared_ptr. 2025-11-20 15:58:53 -05:00
Thomas Harte
923fdd42ec Add a printf for tracking. 2025-11-20 15:35:06 -05:00
Thomas Harte
50cd28f882 Add three device-descriptor-specific functions. 2025-11-20 15:32:57 -05:00
Thomas Harte
7bc865a2e0 Update CMake sources list. 2025-11-20 13:13:32 -05:00
Thomas Harte
1cf3c77ae9 Forward file bundle and host FS traps to the handler. 2025-11-20 13:11:09 -05:00
Thomas Harte
d3cda5d878 Merge pull request #1644 from TomHarte/Vic20KeyboardPresumption
Vic20 keys: avoid manual range test, remove macro.
2025-11-20 12:54:24 -05:00
Thomas Harte
4f09f38f2e Unify naming, record trap addresses. 2025-11-20 12:49:21 -05:00
Thomas Harte
4980caee1b Merge branch 'T' into EXOSDirect 2025-11-20 12:44:33 -05:00
Thomas Harte
ae89c66b17 Transcribe basic function and error codes. 2025-11-20 12:42:56 -05:00
Thomas Harte
bb7059a9e1 KEEPSAKE. 2025-11-20 12:41:41 -05:00
Thomas Harte
05de67ba76 Avoid manual range test, remove macro. 2025-11-19 22:04:12 -05:00
Thomas Harte
25bf7df4d1 Install FILE IO ROM and list out syscall points. 2025-11-19 21:53:53 -05:00
Thomas Harte
e44cbcc1d5 Add to all project files. 2025-11-19 21:08:46 -05:00
Thomas Harte
c876bcb849 Ensure appropriate-looking .com files get to the Enterprise. 2025-11-19 17:50:16 -05:00
Thomas Harte
febff84421 Add file bundles as an undefined concept. 2025-11-19 17:09:55 -05:00
Thomas Harte
1ca261986e New direction: attempt a first sweep with the EP128Emu ROM. 2025-11-19 17:04:39 -05:00
Thomas Harte
71e319a815 Merge branch 'master' into EXOSDirect 2025-11-19 16:24:35 -05:00
Thomas Harte
a67e222c35 Merge pull request #1642 from TomHarte/WriteableD64
Enable writing to D64 images.
2025-11-19 13:31:34 -05:00
Thomas Harte
e173a93b57 Decode and write sectors. 2025-11-19 13:14:13 -05:00
Thomas Harte
3dbf62ca08 Treat lack of a serialiser as at-end. 2025-11-19 13:13:58 -05:00
Thomas Harte
bb5239e553 Move D64 into a position where it needs to decode sectors. 2025-11-19 12:29:57 -05:00
Thomas Harte
faec5c3f84 Merge pull request #1640 from TomHarte/1540Writing
Give the 1540/1541 the ability to write.
2025-11-18 22:59:23 -05:00
Thomas Harte
9c359627f3 Add optional initial shift delay. 2025-11-18 22:47:19 -05:00
Thomas Harte
2a0208c554 Set up more realistic feedback loop. 2025-11-18 18:26:02 -05:00
Thomas Harte
f513edc006 Attempt full write loop. 2025-11-18 17:03:22 -05:00
Thomas Harte
9e39be282b Start to consolidate relationship around push. 2025-11-18 13:12:41 -05:00
Thomas Harte
a9d945d6d2 Merge pull request #1639 from TomHarte/CommodoreNew6502s
Adapt all Commodore machines to 6502Mk2.
2025-11-17 23:16:17 -05:00
Thomas Harte
5e465f1ff4 Avoid function specialisation. 2025-11-17 23:04:33 -05:00
Thomas Harte
5359964fef Make minor style improvements, fix cursor keys. 2025-11-17 22:55:18 -05:00
Thomas Harte
fa8be26f9f Fix read-only bit. 2025-11-17 22:20:20 -05:00
Thomas Harte
aabfe7c284 Observe that there is an attempt to output data. 2025-11-17 21:15:42 -05:00
Thomas Harte
d011b10b5d Update comments, add note-to-self on write mode. 2025-11-17 17:54:09 -05:00
Thomas Harte
332b37063f Adjust for style. 2025-11-17 17:15:26 -05:00
Thomas Harte
b3a9e39be3 Transfer C1540, ensure Plus 4 bus always holds _something_. 2025-11-17 14:39:35 -05:00
Thomas Harte
67590cf06b Adapt Vic-20 to the newer 6502. 2025-11-17 14:23:05 -05:00
Thomas Harte
236fdacb36 Adapt Plus 4 to the newer 6502. 2025-11-17 14:14:25 -05:00
Thomas Harte
176bda9eb8 Merge pull request #1637 from TomHarte/Voice3Off
SID: support the voice 3 disable bit.
2025-11-14 19:35:05 -05:00
Thomas Harte
9f0a0443a8 SID: support the voice 3 disable bit. 2025-11-14 15:38:54 -05:00
Thomas Harte
fd1a7e78c5 Merge pull request #1636 from TomHarte/BiquadAttribution
Record references for the SID and biquad filter.
2025-11-14 13:29:55 -05:00
Thomas Harte
909fa57b27 Record references for the SID and biquad filter. 2025-11-14 13:29:15 -05:00
Thomas Harte
5630b1c351 Merge pull request #1634 from TomHarte/BeebSID
Investigate the SID via BeebSID.
2025-11-13 20:38:34 -05:00
Thomas Harte
c4fe38a61f Allow potentometer inputs to be set; disable SID by default. 2025-11-13 18:05:13 -05:00
Thomas Harte
5b4f303e35 Mention memory barrier. 2025-11-13 18:02:41 -05:00
Thomas Harte
c9c1bde6e2 Switch to spinning on SID thread synchronisation. 2025-11-13 17:59:24 -05:00
Thomas Harte
d01e1f3bb1 Block and synchronise threads for voice 3 readback. 2025-11-13 17:34:57 -05:00
Thomas Harte
fd32e63459 Clean up: pull out noise generation, remove code from header. 2025-11-13 13:44:53 -05:00
Thomas Harte
dbbb1d60fc Add commentary, use filter reconfiguration to retain sample history. 2025-11-13 13:27:55 -05:00
Thomas Harte
1ce013bcf7 Simplify, update noise tap. 2025-11-13 11:59:56 -05:00
Thomas Harte
86bf019aac Attempt further to improve filter precision. 2025-11-13 11:54:37 -05:00
Thomas Harte
d00546dd77 Add post hoc filter, attempt to juice precision. 2025-11-13 07:31:59 -05:00
Thomas Harte
cf33e17688 Attempt to use biquad filter; fix signs. 2025-11-12 23:08:35 -05:00
Thomas Harte
c5c6c5ff72 Add textbook filter construction. 2025-11-12 22:06:48 -05:00
Thomas Harte
fa0835abd8 Capture all filter parameters. 2025-11-12 17:47:49 -05:00
Thomas Harte
f232b179ed Partition channels into filtered and unfiltered, and apply no-op biquad. 2025-11-12 17:40:14 -05:00
Thomas Harte
a4a0026cab Reintroduce decay stage; flip pulse meaning. 2025-11-11 21:38:27 -05:00
Thomas Harte
eac7493180 Support master volume. 2025-11-11 21:04:07 -05:00
Thomas Harte
989fb32fba Fix clocking, do a linear attack phase. 2025-11-11 20:53:54 -05:00
Thomas Harte
735afcfabb Adopt painful pulse test, temporarily (?). 2025-11-11 18:26:00 -05:00
Thomas Harte
37152a1fad Start testing; I'm now unsure about pulses. 2025-11-11 17:54:31 -05:00
Thomas Harte
4e86184955 Add local hack to ensure good flushing. 2025-11-11 14:40:13 -05:00
Thomas Harte
d23dbb96c2 Support system volume, avoid clipping. 2025-11-11 14:40:04 -05:00
Thomas Harte
4586e4b4c1 Apply envelope. 2025-11-11 14:26:53 -05:00
Thomas Harte
de5cdbf18c Make a complete attempt at ADSR. 2025-11-11 14:25:36 -05:00
Thomas Harte
8c2294fc0d Treat sustain as a volume; start second prescaler table. 2025-11-11 12:43:48 -05:00
Thomas Harte
b0b82782ad Build in initial prescaler. 2025-11-11 12:21:49 -05:00
Thomas Harte
b9f5802c89 Return whatever was written last if read. 2025-11-11 09:19:01 -05:00
Thomas Harte
29235f1276 Adjust noise clocking, make it reactive to the test bit. 2025-11-11 09:16:43 -05:00
Thomas Harte
8c74e2a323 Implement LFSR. 2025-11-10 22:44:00 -05:00
Thomas Harte
ae2936b9c3 Correct clock rate, triangle wave. 2025-11-10 22:35:13 -05:00
Thomas Harte
0d295a6338 Don't capture a reference to parameters. 2025-11-10 22:10:28 -05:00
Thomas Harte
3ebd6c6871 Rejig oscillators, output some vague noise. 2025-11-10 21:52:10 -05:00
Thomas Harte
6e2cd0ace6 Divide state, start adding waveforms. 2025-11-10 17:27:32 -05:00
Thomas Harte
af82a0bcda Add ADSR TODO. 2025-11-10 14:18:24 -05:00
Thomas Harte
6fe208ae77 Honour test and sync bits. 2025-11-10 14:17:54 -05:00
Thomas Harte
f569b86c90 Merge branch 'master' into BeebSID 2025-11-10 14:10:33 -05:00
Thomas Harte
7dfd5ea0d0 Add phase accumulation, rename to pitch. 2025-11-10 13:27:43 -05:00
Thomas Harte
0cc5a9d74f Move thread. 2025-11-08 23:02:54 -05:00
Thomas Harte
5e98e6502d Attempt some basic voice details. 2025-11-08 21:54:41 -05:00
Thomas Harte
fe7a206fc5 Add an empty vessel of a SID. 2025-11-07 22:51:28 -05:00
Thomas Harte
d628f75244 Switch to using an assembler. 2025-04-27 14:24:11 -04:00
Thomas Harte
cd09b5d356 Merge branch 'master' into EXOSDirect 2025-04-27 14:23:53 -04:00
Thomas Harte
bb2cf0170d Start trying to draft an EXOS device ROM. 2025-04-24 20:49:54 -04:00
63 changed files with 2718 additions and 501 deletions

View File

@@ -44,6 +44,7 @@ struct BBCMicroTarget: public ::Analyser::Static::Target, public Reflection::Str
bool has_1770dfs = false;
bool has_adfs = false;
bool has_sideways_ram = true;
bool has_beebsid = false;
ReflectableEnum(TubeProcessor, None, WDC65C02, Z80);
TubeProcessor tube_processor = TubeProcessor::None;
@@ -56,6 +57,7 @@ private:
DeclareField(has_1770dfs);
DeclareField(has_adfs);
DeclareField(has_sideways_ram);
DeclareField(has_beebsid);
AnnounceEnum(TubeProcessor);
DeclareField(tube_processor);
}

View File

@@ -33,7 +33,7 @@ Analyser::Static::TargetList Analyser::Static::Enterprise::GetTargets(
bool
) {
// This analyser can comprehend disks only.
if(media.disks.empty()) return {};
if(media.disks.empty() && media.file_bundles.empty()) return {};
// Otherwise, assume a return will happen.
Analyser::Static::TargetList targets;
@@ -86,7 +86,36 @@ Analyser::Static::TargetList Analyser::Static::Enterprise::GetTargets(
}
}
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;
}

View File

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

View File

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

View File

@@ -325,7 +325,7 @@ public:
/// Flushes all accumulated time.
inline void flush() {
if(!is_flushed_) {
task_queue_.flush();
task_queue_.lock_flush();
object_.run_for(time_since_update_.template flush<TargetTimeScale>());
is_flushed_ = true;
}

View File

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

View File

@@ -15,7 +15,6 @@ using namespace MOS::MOS6560;
AudioGenerator::AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) :
audio_queue_(audio_queue) {}
void AudioGenerator::set_volume(const uint8_t volume) {
audio_queue_.enqueue([this, volume]() {
volume_ = int16_t(volume) * range_multiplier_;
@@ -148,7 +147,7 @@ template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Store>(
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(
std::size_t, Outputs::Speaker::MonoSample *);
void AudioGenerator::set_sample_volume_range(std::int16_t range) {
void AudioGenerator::set_sample_volume_range(const std::int16_t range) {
range_multiplier_ = int16_t(range / 64);
}

View File

@@ -75,7 +75,7 @@ public:
}
~MOS6560() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
void set_clock_rate(const double clock_rate) {

View File

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

View File

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

View File

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

433
Components/SID/SID.cpp Normal file
View File

@@ -0,0 +1,433 @@
//
// SID.cpp
// Clock Signal
//
// Created by Thomas Harte on 07/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#include "SID.hpp"
// Sources used:
//
// (1) SID Article v0.2 at https://github.com/ImreOlajos/SID-Article
// (2) Technical SID Information/Software stuff at http://www.sidmusic.org/sid/sidtech2.html
// (3) SID 6581/8580 (Sound Interface Device) reference at https://oxyron.de/html/registers_sid.html
using namespace MOS::SID;
SID::SID(Concurrency::AsyncTaskQueue<false> &audio_queue) :
audio_queue_(audio_queue),
output_filter_(
SignalProcessing::BiquadFilter::Type::LowPass,
1000000.0f,
15000.0f
) {}
// MARK: - Programmer interface.
void SID::write(const Numeric::SizedInt<5> address, const uint8_t value) {
last_write_ = value;
audio_queue_.enqueue([=, this] {
const auto voice = [&]() -> Voice & {
return voices_[address.get() / 7];
};
const auto oscillator = [&]() -> Voice::Oscillator & {
return voice().oscillator;
};
const auto adsr = [&]() -> Voice::ADSR & {
return voice().adsr;
};
switch(address.get()) {
case 0x00: case 0x07: case 0x0e:
oscillator().pitch = (oscillator().pitch & 0xff'00'00) | uint32_t(value << 8);
break;
case 0x01: case 0x08: case 0x0f:
oscillator().pitch = (oscillator().pitch & 0x00'ff'00) | uint32_t(value << 16);
break;
case 0x02: case 0x09: case 0x10:
oscillator().pulse_width = (oscillator().pitch & 0xf0'00'00'00) | uint32_t(value << 20);
break;
case 0x03: case 0x0a: case 0x11:
// The top bit of the phase counter is inverted; since it'll be compared directly with the
// pulse width, invert that bit too.
oscillator().pulse_width =
(
(oscillator().pitch & 0x0f'f0'00'00) |
uint32_t(value << 28)
);
break;
case 0x04: case 0x0b: case 0x12:
voice().set_control(value);
break;
case 0x05: case 0x0c: case 0x13:
adsr().attack = value >> 4;
adsr().decay = value;
adsr().set_phase(adsr().phase);
break;
case 0x06: case 0x0d: case 0x14:
adsr().sustain = (value >> 4) | (value & 0xf0);
adsr().release = value;
adsr().set_phase(adsr().phase);
break;
case 0x15:
filter_cutoff_.load<0, 3>(value);
update_filter();
break;
case 0x16:
filter_cutoff_.load<3>(value);
update_filter();
break;
case 0x17:
filter_channels_ = value;
filter_resonance_ = value >> 4;
update_filter();
break;
case 0x18:
volume_ = value & 0x0f;
filter_mode_ = value >> 4;
voice3_disable_ = value & 0x80;
update_filter();
break;
}
});
}
void SID::set_potentometer_input(const int index, const uint8_t value) {
potentometers_[index] = value;
}
void SID::update_filter() {
using Type = SignalProcessing::BiquadFilter::Type;
Type type = Type::AllPass;
switch(filter_mode_.get()) {
case 0:
filter_ = SignalProcessing::BiquadFilter();
return;
case 1:
case 3: type = Type::LowPass; break;
case 2: type = Type::BandPass; break;
case 5: type = Type::Notch; break;
case 4:
case 6: type = Type::HighPass; break;
case 7: type = Type::AllPass; break;
}
filter_.configure(
type,
1'000'000.0f,
30.0f + float(filter_cutoff_.get()) * 5.8f,
0.707f + float(filter_resonance_.get()) * 0.2862f,
6.0f,
true
);
// Filter cutoff: the data sheet provides that it is linear, and "approximate Cutoff Frequency
// ranges between 30Hz and 12KHz [with recommended externally-supplied capacitors]."
//
// It's an 11-bit number, so the above is "approximate"ly right.
// Resonance: a complete from-thin-air guess. The data sheet says merely:
//
// "There are 16 Resonance settings ranging from about 0.707 (Critical Damping) for a count of 0
// to a maximum for a count of 15"
//
// i.e. no information is given on the maximum. I've taken it to be 5-ish per commentary on more general sites
// that 5 is a typical ceiling for the resonance factor.
}
uint8_t SID::read(const Numeric::SizedInt<5> address) {
switch(address.get()) {
default: return last_write_;
case 0x19: return potentometers_[0];
case 0x1a: return potentometers_[1];
case 0x1b:
case 0x1c:
// Ensure all channels are entirely up to date.
audio_queue_.spin_flush();
return (address == 0x1c) ? voices_[2].adsr.envelope : uint8_t(voices_[2].output(voices_[1]) >> 4);
}
}
// MARK: - Oscillators.
void Voice::Oscillator::reset_phase() {
phase = PhaseReload;
}
bool Voice::Oscillator::did_raise_b23() const {
return previous_phase > phase;
}
bool Voice::Oscillator::did_raise_b19() const {
static constexpr int NoiseBit = 1 << (19 + 8);
return (previous_phase ^ phase) & phase & NoiseBit;
}
uint16_t Voice::Oscillator::sawtooth_output() const {
return (phase >> 20) ^ 0x800;
}
// MARK: - Noise generator.
uint16_t Voice::NoiseGenerator::output() const {
// Uses bits: 20, 18, 14, 11, 9, 5, 2 and 0, plus four more zero bits.
const uint16_t output =
((noise >> 9) & 0b1000'0000'0000) | // b20 -> b11
((noise >> 8) & 0b0100'0000'0000) | // b18 -> b10
((noise >> 5) & 0b0010'0000'0000) | // b14 -> b9
((noise >> 3) & 0b0001'0000'0000) | // b11 -> b8
((noise >> 2) & 0b0000'1000'0000) | // b9 -> b7
((noise << 1) & 0b0000'0100'0000) | // b5 -> b6
((noise << 3) & 0b0000'0010'0000) | // b2 -> b5
((noise << 4) & 0b0000'0001'0000); // b0 -> b4
assert(output <= Voice::MaxWaveformValue);
return output;
}
void Voice::NoiseGenerator::update(const bool test) {
noise =
(noise << 1) |
(((noise >> 17) ^ ((noise >> 22) | test)) & 1);
}
// MARK: - ADSR.
void Voice::ADSR::set_phase(const Phase new_phase) {
static constexpr uint16_t rate_prescaler[] = {
9, 32, 63, 95, 149, 220, 267, 313, 392, 977, 1954, 3126, 3907, 11720, 19532, 31251
};
static_assert(sizeof(rate_prescaler) / sizeof(*rate_prescaler) == 16);
phase = new_phase;
switch(phase) {
case Phase::Attack: rate_counter_target = rate_prescaler[attack.get()]; break;
case Phase::DecayAndHold: rate_counter_target = rate_prescaler[decay.get()]; break;
case Phase::Release: rate_counter_target = rate_prescaler[release.get()]; break;
}
}
// MARK: - Voices.
void Voice::set_control(const uint8_t new_control) {
const bool old_gate = gate();
control = new_control;
if(gate() && !old_gate) {
adsr.set_phase(ADSR::Phase::Attack);
} else if(!gate() && old_gate) {
adsr.set_phase(ADSR::Phase::Release);
}
}
bool Voice::noise() const { return control.bit<7>(); }
bool Voice::pulse() const { return control.bit<6>(); }
bool Voice::sawtooth() const { return control.bit<5>(); }
bool Voice::triangle() const { return control.bit<4>(); }
bool Voice::test() const { return control.bit<3>(); }
bool Voice::ring_mod() const { return control.bit<2>(); }
bool Voice::sync() const { return control.bit<1>(); }
bool Voice::gate() const { return control.bit<0>(); }
void Voice::update() {
// Oscillator.
oscillator.previous_phase = oscillator.phase;
if(test()) {
oscillator.phase = 0;
} else {
oscillator.phase += oscillator.pitch;
if(oscillator.did_raise_b19()) {
noise_generator.update(test());
}
}
// ADSR.
// First prescalar, which is a function of the programmer-set rate.
++ adsr.rate_counter;
if(adsr.rate_counter == adsr.rate_counter_target) {
adsr.rate_counter = 0;
// Second prescalar, which approximates an exponential.
static constexpr uint8_t exponential_prescaler[] = {
1, // 0
30, 30, 30, 30, 30, 30, // 16
16, 16, 16, 16, 16, 16, 16, 16, // 714
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 1526
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // 2754
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 5594
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1,
};
static_assert(sizeof(exponential_prescaler) == 256);
static_assert(exponential_prescaler[0] == 1);
static_assert(exponential_prescaler[1] == 30);
static_assert(exponential_prescaler[6] == 30);
static_assert(exponential_prescaler[7] == 16);
static_assert(exponential_prescaler[14] == 16);
static_assert(exponential_prescaler[15] == 8);
static_assert(exponential_prescaler[26] == 8);
static_assert(exponential_prescaler[27] == 4);
static_assert(exponential_prescaler[54] == 4);
static_assert(exponential_prescaler[55] == 2);
static_assert(exponential_prescaler[94] == 2);
static_assert(exponential_prescaler[95] == 1);
static_assert(exponential_prescaler[255] == 1);
if(adsr.phase == ADSR::Phase::Attack) {
++adsr.envelope;
// TODO: what really resets the exponential counter? If anything?
adsr.exponential_counter = 0;
if(adsr.envelope == 0xff) {
adsr.set_phase(ADSR::Phase::DecayAndHold);
}
} else {
++adsr.exponential_counter;
if(adsr.exponential_counter == exponential_prescaler[adsr.envelope]) {
adsr.exponential_counter = 0;
if(adsr.envelope && (adsr.envelope != adsr.sustain || adsr.phase != ADSR::Phase::DecayAndHold)) {
--adsr.envelope;
}
}
}
}
}
void Voice::synchronise(const Voice &prior) {
// Only oscillator work to do here.
if(
sync() &&
prior.oscillator.did_raise_b23()
) {
oscillator.phase = Oscillator::PhaseReload;
}
}
uint16_t Voice::pulse_output() const {
return (
(oscillator.phase ^ 0x8000'0000) < oscillator.pulse_width
) ? 0 : MaxWaveformValue;
}
uint16_t Voice::triangle_output(const Voice &prior) const {
const uint16_t sawtooth = oscillator.sawtooth_output();
const uint16_t xor_mask1 = sawtooth;
const uint16_t xor_mask2 = ring_mod() ? prior.sawtooth() : 0;
const uint16_t xor_mask = ((xor_mask1 ^ xor_mask2) & 0x800) ? 0xfff : 0x000;
return ((sawtooth << 1) ^ xor_mask) & 0xfff;
}
uint16_t Voice::output(const Voice &prior) const {
// TODO: true composite waves.
//
// My current understanding on this: if multiple waveforms are enabled, the pull to zero beats the
// pull to one on any line where the two compete. But the twist is that the lines are not necessarily
// one per bit since they lead to a common ground. Ummm, I think.
//
// Anyway, first pass: logical AND. It's not right. It will temporarily do.
uint16_t output = MaxWaveformValue;
if(pulse()) output &= pulse_output();
if(sawtooth()) output &= oscillator.sawtooth_output();
if(triangle()) output &= triangle_output(prior);
if(noise()) output &= noise_generator.output();
return (output * adsr.envelope) / 255;
}
// MARK: - Wave generation
void SID::set_sample_volume_range(const std::int16_t range) {
range_ = range;
}
bool SID::is_zero_level() const {
return false;
}
template <Outputs::Speaker::Action action>
void SID::apply_samples(const std::size_t number_of_samples, Outputs::Speaker::MonoSample *const target) {
for(std::size_t c = 0; c < number_of_samples; c++) {
// Advance phase.
voices_[0].update();
voices_[1].update();
voices_[2].update();
// Apply hard synchronisations.
voices_[0].synchronise(voices_[2]);
voices_[1].synchronise(voices_[0]);
voices_[2].synchronise(voices_[1]);
// Construct filtered and unfiltered output.
const uint16_t outputs[3] = {
voices_[0].output(voices_[2]),
voices_[1].output(voices_[0]),
voices_[2].output(voices_[1]),
};
const uint16_t direct_sample =
(filter_channels_.bit<0>() ? 0 : outputs[0]) +
(filter_channels_.bit<1>() ? 0 : outputs[1]) +
(filter_channels_.bit<2>() || voice3_disable_ ? 0 : outputs[2]);
const int16_t filtered_sample =
filter_.apply(
(filter_channels_.bit<0>() ? outputs[0] : 0) +
(filter_channels_.bit<1>() ? outputs[1] : 0) +
(filter_channels_.bit<2>() ? outputs[2] : 0)
);
// Sum, apply volume and output.
const auto sample = output_filter_.apply(int16_t(
(
volume_ * (
direct_sample +
filtered_sample
- 227 // DC offset.
)
- 88732
) / 3
));
// Maximum range of above: 15 * (4095 * 3 - 227) = [-3405, 180870]
// So subtracting 88732 will move to the centre of the range, and 3 is the smallest
// integer that avoids clipping.
Outputs::Speaker::apply<action>(
target[c],
Outputs::Speaker::MonoSample((sample * range_) >> 16)
);
}
}
template void SID::apply_samples<Outputs::Speaker::Action::Mix>(
std::size_t, Outputs::Speaker::MonoSample *);
template void SID::apply_samples<Outputs::Speaker::Action::Store>(
std::size_t, Outputs::Speaker::MonoSample *);
template void SID::apply_samples<Outputs::Speaker::Action::Ignore>(
std::size_t, Outputs::Speaker::MonoSample *);

129
Components/SID/SID.hpp Normal file
View File

@@ -0,0 +1,129 @@
//
// SID.hpp
// Clock Signal
//
// Created by Thomas Harte on 07/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Numeric/SizedInt.hpp"
#include "Concurrency/AsyncTaskQueue.hpp"
#include "Outputs/Speaker/Implementation/BufferSource.hpp"
#include "SignalProcessing/BiquadFilter.hpp"
namespace MOS::SID {
struct Voice {
static constexpr uint16_t MaxWaveformValue = (1 << 12) - 1;
struct Oscillator {
// Programmer inputs.
uint32_t pitch = 0;
uint32_t pulse_width = 0;
// State.
//
// A real SID has a 24-bit phase counter and does various things when the top bit transitions from 0 to 1.
// This implementation maintains a 32-bit phase counter in which the low byte is unused and the top bit
// is inverted. That saves the cost of any masking and makes the 0 -> 1 transition test actually a 1 -> 0
// transition test, which can be phrased simply as after < before. Sadly overflow of signed integers is
// still undefined behaviour in C++ at the time of writing.
static constexpr uint32_t PhaseReload = 0x8000'0000;
uint32_t phase = PhaseReload;
uint32_t previous_phase = PhaseReload;
void reset_phase();
bool did_raise_b23() const;
bool did_raise_b19() const;
uint16_t sawtooth_output() const;
} oscillator;
struct ADSR {
// Programmer inputs.
Numeric::SizedInt<4> attack;
Numeric::SizedInt<4> decay;
Numeric::SizedInt<4> release;
Numeric::SizedInt<8> sustain;
// State.
enum class Phase {
Attack,
DecayAndHold,
Release,
} phase = Phase::Release;
Numeric::SizedInt<15> rate_counter;
Numeric::SizedInt<15> rate_counter_target;
uint8_t exponential_counter;
uint8_t envelope;
void set_phase(const Phase);
} adsr;
struct NoiseGenerator {
static constexpr uint32_t NoiseReload = 0x7'ffff;
uint32_t noise = NoiseReload;
uint16_t output() const;
void update(const bool test);
} noise_generator;
void set_control(const uint8_t);
void update();
void synchronise(const Voice &prior);
uint16_t output(const Voice &prior) const;
private:
Numeric::SizedInt<8> control;
bool noise() const;
bool pulse() const;
bool sawtooth() const;
bool triangle() const;
bool test() const;
bool ring_mod() const;
bool sync() const;
bool gate() const;
uint16_t pulse_output() const;
uint16_t triangle_output(const Voice &prior) const;
};
class SID: public Outputs::Speaker::BufferSource<SID, false> {
public:
SID(Concurrency::AsyncTaskQueue<false> &audio_queue);
void write(Numeric::SizedInt<5> address, uint8_t value);
uint8_t read(Numeric::SizedInt<5> address);
void set_potentometer_input(int index, uint8_t value);
// Outputs::Speaker::BufferSource.
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t, Outputs::Speaker::MonoSample *);
bool is_zero_level() const;
void set_sample_volume_range(std::int16_t);
private:
Concurrency::AsyncTaskQueue<false> &audio_queue_;
Voice voices_[3];
uint8_t last_write_;
int16_t range_ = 0;
uint8_t volume_ = 0;
SignalProcessing::BiquadFilter filter_;
Numeric::SizedInt<11> filter_cutoff_;
Numeric::SizedInt<4> filter_resonance_;
Numeric::SizedInt<4> filter_channels_;
Numeric::SizedInt<3> filter_mode_;
bool voice3_disable_;
void update_filter();
SignalProcessing::BiquadFilter output_filter_;
uint8_t potentometers_[2]{};
};
}

View File

@@ -97,6 +97,12 @@ public:
}
}
/// @returns The number of items currently enqueued.
size_t size() {
const std::lock_guard guard(condition_mutex_);
return actions_.size();
}
/// Causes any enqueued actions that are not yet scheduled to be scheduled.
void perform() {
static_assert(!perform_automatically);
@@ -131,7 +137,7 @@ public:
/// Schedules any remaining unscheduled work, then blocks synchronously
/// until all scheduled work has been performed.
void flush() {
void lock_flush() {
std::mutex flush_mutex;
std::condition_variable flush_condition;
bool has_run = false;
@@ -150,6 +156,23 @@ public:
flush_condition.wait(lock, [&has_run] { return has_run; });
}
/// Schedules any remaining unscheduled work, then spins
/// until all scheduled work has been performed, placing a memory barrier
/// in between.
void spin_flush() {
std::atomic<bool> has_run = false;
enqueue([&has_run] () {
has_run.store(true, std::memory_order::release);
});
if constexpr (!perform_automatically) {
perform();
}
while(!has_run.load(std::memory_order::acquire));
}
~AsyncTaskQueue() {
stop();
}

View File

@@ -24,6 +24,7 @@
#include "Components/6522/6522.hpp"
#include "Components/6845/CRTC6845.hpp"
#include "Components/6850/6850.hpp"
#include "Components/SID/SID.hpp"
#include "Components/SAA5050/SAA5050.hpp"
#include "Components/SN76489/SN76489.hpp"
#include "Components/uPD7002/uPD7002.hpp"
@@ -35,6 +36,7 @@
#include "Outputs/Log.hpp"
#include "Outputs/CRT/CRT.hpp"
#include "Outputs/Speaker/Implementation/CompoundSource.hpp"
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "Concurrency/AsyncTaskQueue.hpp"
@@ -107,22 +109,48 @@ private:
/*!
Combines an SN76489 with an appropriate asynchronous queue and filtering speaker.
*/
// TODO: generalise the below and clean up across the project.
template <bool has_beebsid>
struct Audio {
private:
using CompoundSource = Outputs::Speaker::CompoundSource<TI::SN76489, MOS::SID::SID>;
using Source = std::conditional_t<has_beebsid, CompoundSource, TI::SN76489>;
using Speaker = Outputs::Speaker::PullLowpass<Source>;
Source &speaker_source() {
if constexpr (has_beebsid) {
return compound_;
} else {
return sn76489_;
}
}
public:
Audio() :
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, 2),
speaker_(sn76489_)
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, 4),
sid_(audio_queue_),
compound_(sn76489_, sid_),
speaker_(speaker_source())
{
// Combined with the additional divider specified above, implies this chip is clocked at 4Mhz.
speaker_.set_input_rate(2'000'000.0f);
// Combined with the additional divider specified above, implies the SN76489 is clocked at 4Mhz.
speaker_.set_input_rate(1'000'000.0f);
}
~Audio() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
TI::SN76489 *operator ->() {
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
return &sn76489_;
template <typename TargetT>
TargetT &get() {
post_time();
if constexpr (std::is_same_v<TargetT, TI::SN76489>) {
return sn76489_;
}
if constexpr (std::is_same_v<TargetT, MOS::SID::SID>) {
return sid_;
}
}
void operator +=(const Cycles duration) {
@@ -130,7 +158,7 @@ struct Audio {
}
void flush() {
speaker_.run_for(audio_queue_, time_since_update_.flush<Cycles>());
post_time();
audio_queue_.perform();
}
@@ -138,10 +166,20 @@ struct Audio {
return &speaker_;
}
size_t queue_size() {
return audio_queue_.size();
}
private:
void post_time() {
speaker_.run_for(audio_queue_, time_since_update_.divide(Cycles(2)));
}
Concurrency::AsyncTaskQueue<false> audio_queue_;
TI::SN76489 sn76489_;
Outputs::Speaker::PullLowpass<TI::SN76489> speaker_;
MOS::SID::SID sid_;
CompoundSource compound_;
Outputs::Speaker::PullLowpass<Source> speaker_;
Cycles time_since_update_;
};
@@ -174,19 +212,20 @@ protected:
/*!
Models the system VIA, which connects to the SN76489 and the keyboard.
*/
struct SystemVIAPortHandler;
using SystemVIA = MOS::MOS6522::MOS6522<SystemVIAPortHandler>;
struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
struct Delegate {
virtual void strobe_lightpen() = 0;
};
struct VSyncReceiver {
virtual void set_vsync(bool) = 0;
};
struct SystemVIADelegate {
virtual void strobe_lightpen() = 0;
};
template <typename AudioT>
struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler, public VSyncReceiver {
SystemVIAPortHandler(
Audio &audio,
AudioT &audio,
VideoBaseAddress &video_base,
SystemVIA &via,
Delegate &delegate,
MOS::MOS6522::MOS6522<SystemVIAPortHandler<AudioT>> &via,
SystemVIADelegate &delegate,
const std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks,
const bool run_disk
) :
@@ -221,7 +260,7 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
// Check for a strobe on the audio output.
if((old_latch^latch_) & old_latch & LatchFlags::WriteToSN76489) {
audio_->write(port_a_output_);
audio_.template get<TI::SN76489>().write(port_a_output_);
}
// Pass on the video wraparound/base.
@@ -335,10 +374,10 @@ private:
uint8_t port_a_output_ = 0;
bool previous_cb2_ = false;
Audio &audio_;
AudioT &audio_;
VideoBaseAddress &video_base_;
SystemVIA &via_;
MOS::MOS6522::MOS6522<SystemVIAPortHandler<AudioT>> &via_;
// MARK: - Keyboard state and helpers.
@@ -369,7 +408,7 @@ private:
}
} ()).to_ulong() & 0xfe; // Discard the first row.
via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two>(state);
via_.template set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two>(state);
}
static inline const std::string caps_led = "CAPS";
@@ -379,7 +418,11 @@ private:
Activity::Observer *activity_observer_ = nullptr;
const std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks_;
Delegate &delegate_;
SystemVIADelegate &delegate_;
void set_vsync(const bool vsync) override {
via_.template set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(vsync);
}
};
// MARK: - CRTC output.
@@ -389,10 +432,10 @@ private:
*/
class CRTCBusHandler: public VideoBaseAddress {
public:
CRTCBusHandler(const uint8_t *const ram, SystemVIA &system_via) :
CRTCBusHandler(const uint8_t *const ram, VSyncReceiver &vsync_receiver) :
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1),
ram_(ram),
system_via_(system_via)
vsync_receiver_(vsync_receiver)
{}
void set_dynamic_framing(const bool enable) {
@@ -440,7 +483,10 @@ public:
static_assert(!(PixelAllocationUnit % 16));
static_assert(!(PixelAllocationUnit % 12));
system_via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(state.vsync);
if(state.vsync != vsync_) {
vsync_receiver_.set_vsync(state.vsync);
vsync_ = state.vsync;
}
// Count cycles since horizontal sync to insert a colour burst.
// TODO: this is copy/pasted from the CPC. How does the BBC do it?
@@ -675,7 +721,8 @@ private:
}
const uint8_t *const ram_ = nullptr;
SystemVIA &system_via_;
VSyncReceiver &vsync_receiver_;
bool vsync_ = false;
Mullard::SAA5050Serialiser saa5050_serialiser_;
};
@@ -705,7 +752,7 @@ struct Tube<HostT, TubeProcessor::None> {
// MARK: - ConcreteMachine.
template <TubeProcessor tube_processor, bool has_1770>
template <TubeProcessor tube_processor, bool has_1770, bool has_beebsid>
class ConcreteMachine:
public Activity::Source,
public Configurable::Device,
@@ -718,7 +765,7 @@ class ConcreteMachine:
public MachineTypes::TimedMachine,
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
public NEC::uPD7002::Delegate,
public SystemVIAPortHandler::Delegate,
public SystemVIADelegate,
public Utility::TypeRecipient<CharacterMapper>,
public WD::WD1770::Delegate
{
@@ -731,7 +778,7 @@ public:
system_via_port_handler_(audio_, crtc_bus_handler_, system_via_, *this, joysticks_, target.should_shift_restart),
user_via_(user_via_port_handler_),
system_via_(system_via_port_handler_),
crtc_bus_handler_(ram_.data(), system_via_),
crtc_bus_handler_(ram_.data(), system_via_port_handler_),
crtc_(crtc_bus_handler_),
acia_(HalfCycles(2'000'000)), // TODO: look up real ACIA clock rate.
adc_(HalfCycles(2'000'000)),
@@ -990,6 +1037,12 @@ public:
}
break;
}
} else if(has_beebsid && address >= 0xfc20 && address < 0xfc40) {
if constexpr (is_read(operation)) {
value = audio_.template get<MOS::SID::SID>().read(+address);
} else {
audio_.template get<MOS::SID::SID>().write(+address, value);
}
} else {
Logger::error()
.append("Unhandled IO %s at %04x", is_read(operation) ? "read" : "write", address)
@@ -1045,8 +1098,7 @@ private:
return crtc_bus_handler_.get_scaled_scan_status();
}
// MARK: - SystemVIAPortHandler::Delegate.
// MARK: - SystemVIADelegate.
void strobe_lightpen() override {
crtc_.trigger_light_pen();
}
@@ -1129,7 +1181,10 @@ private:
}
void flush_output(const int outputs) final {
if(outputs & Output::Audio) {
// TODO: I think there's an infrastructural bug here on macOS; if the audio output has stalled out,
// the outer wrapper won't ask for an audio flush, which means the queue will never try to start,
// and the audio queue will just fill indefinitely. Could this be the mythical 'leak'?
if(outputs & Output::Audio || audio_.queue_size() > 200) {
audio_.flush();
}
}
@@ -1141,7 +1196,7 @@ private:
// MARK: - uPD7002::Delegate.
void did_change_interrupt_status(NEC::uPD7002 &) override {
system_via_.set_control_line_input<MOS::MOS6522::Port::B, MOS::MOS6522::Line::One>(adc_.interrupt());
system_via_.template set_control_line_input<MOS::MOS6522::Port::B, MOS::MOS6522::Line::One>(adc_.interrupt());
}
// MARK: - MediaTarget.
@@ -1200,9 +1255,9 @@ private:
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
UserVIAPortHandler user_via_port_handler_;
SystemVIAPortHandler system_via_port_handler_;
SystemVIAPortHandler<Audio<has_beebsid>> system_via_port_handler_;
UserVIA user_via_;
SystemVIA system_via_;
MOS::MOS6522::MOS6522<SystemVIAPortHandler<Audio<has_beebsid>>> system_via_;
void update_irq_line() {
const bool tube_irq =
@@ -1221,7 +1276,7 @@ private:
);
}
Audio audio_;
Audio<has_beebsid> audio_;
CRTCBusHandler crtc_bus_handler_;
CRTC crtc_;
@@ -1275,12 +1330,21 @@ using namespace BBCMicro;
namespace {
using Target = Analyser::Static::Acorn::BBCMicroTarget;
template <Target::TubeProcessor processor, bool has_1770>
std::unique_ptr<Machine> machine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) {
if(target.has_beebsid) {
return std::make_unique<BBCMicro::ConcreteMachine<processor, has_1770, true>>(target, rom_fetcher);
} else {
return std::make_unique<BBCMicro::ConcreteMachine<processor, has_1770, false>>(target, rom_fetcher);
}
}
template <Target::TubeProcessor processor>
std::unique_ptr<Machine> machine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) {
if(target.has_1770dfs || target.has_adfs) {
return std::make_unique<BBCMicro::ConcreteMachine<processor, true>>(target, rom_fetcher);
return machine<processor, true>(target, rom_fetcher);
} else {
return std::make_unique<BBCMicro::ConcreteMachine<processor, false>>(target, rom_fetcher);
return machine<processor, false>(target, rom_fetcher);
}
}
}

View File

@@ -143,7 +143,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
void set_key_state(uint16_t key, bool isPressed) final {

View File

@@ -127,7 +127,7 @@ public:
}
~AYDeferrer() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
/// Adds @c half_cycles half cycles to the amount of time that has passed.

View File

@@ -678,7 +678,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {

View File

@@ -210,7 +210,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
void run_for(const Cycles cycles) override {

View File

@@ -172,7 +172,7 @@ public:
}
~ConcreteMachine() {
audio_.queue.flush();
audio_.queue.lock_flush();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {

View File

@@ -26,7 +26,7 @@ public:
speaker_(tia_sound_) {}
virtual ~Bus() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
virtual void run_for(const Cycles cycles) = 0;

View File

@@ -144,7 +144,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
// MARK: CRTMachine::Machine

View File

@@ -168,7 +168,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {

View File

@@ -17,7 +17,10 @@
using namespace Commodore::C1540;
namespace {
ROM::Name rom_name(Personality personality) {
// MARK: - Construction, including ROM requests.
ROM::Name rom_name(const Personality personality) {
switch(personality) {
default:
case Personality::C1540: return ROM::Name::Commodore1540;
@@ -26,11 +29,11 @@ ROM::Name rom_name(Personality personality) {
}
}
ROM::Request Machine::rom_request(Personality personality) {
ROM::Request Machine::rom_request(const Personality personality) {
return ROM::Request(rom_name(personality));
}
MachineBase::MachineBase(Personality personality, const ROM::Map &roms) :
MachineBase::MachineBase(const Personality personality, const ROM::Map &roms) :
Storage::Disk::Controller(1000000),
m6502_(*this),
drive_VIA_(drive_VIA_port_handler_),
@@ -58,14 +61,13 @@ MachineBase::MachineBase(Personality personality, const ROM::Map &roms) :
std::memcpy(rom_, rom->second.data(), std::min(sizeof(rom_), rom->second.size()));
}
Machine::Machine(Personality personality, const ROM::Map &roms) :
Machine::Machine(const Personality personality, const ROM::Map &roms) :
MachineBase(personality, roms) {}
void Machine::set_serial_bus(Commodore::Serial::Bus &serial_bus) {
Commodore::Serial::attach(serial_port_, serial_bus);
}
// MARK: - 6502 bus.
Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
Cycles MachineBase::perform(const AddressT address, CPU::MOS6502Mk2::data_t<operation> value) {
/*
Memory map (given that I'm unsure yet on any potential mirroring):
@@ -75,24 +77,28 @@ Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation,
0xc000-0xffff ROM
*/
if(address < 0x800) {
if(is_read(operation))
*value = ram_[address];
if constexpr (is_read(operation))
value = ram_[address];
else
ram_[address] = *value;
ram_[address] = value;
} else if(address >= 0xc000) {
if(is_read(operation)) {
*value = rom_[address & 0x3fff];
if constexpr (is_read(operation)) {
value = rom_[address & 0x3fff];
}
} else if(address >= 0x1800 && address <= 0x180f) {
if(is_read(operation))
*value = serial_port_VIA_.read(address);
if constexpr (is_read(operation))
value = serial_port_VIA_.read(address);
else
serial_port_VIA_.write(address, *value);
serial_port_VIA_.write(address, value);
} else if(address >= 0x1c00 && address <= 0x1c0f) {
if(is_read(operation))
*value = drive_VIA_.read(address);
if constexpr (is_read(operation))
value = drive_VIA_.read(address);
else
drive_VIA_.write(address, *value);
drive_VIA_.write(address, value);
} else {
if constexpr (is_read(operation)) {
value = 0xff;
}
}
serial_port_VIA_.run_for(Cycles(1));
@@ -101,34 +107,39 @@ Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation,
return Cycles(1);
}
void Machine::set_disk(std::shared_ptr<Storage::Disk::Disk> disk) {
get_drive().set_disk(disk);
}
void Machine::run_for(const Cycles cycles) {
m6502_.run_for(cycles);
const bool drive_motor = drive_VIA_port_handler_.get_motor_enabled();
get_drive().set_motor_on(drive_motor);
if(drive_motor)
if(get_drive().get_motor_on()) {
Storage::Disk::Controller::run_for(cycles);
}
}
void MachineBase::set_activity_observer(Activity::Observer *observer) {
// MARK: - External attachments.
void Machine::set_serial_bus(Commodore::Serial::Bus &serial_bus) {
Commodore::Serial::attach(serial_port_, serial_bus);
}
void Machine::set_disk(std::shared_ptr<Storage::Disk::Disk> disk) {
get_drive().set_disk(disk);
drive_VIA_port_handler_.set_is_read_only(disk->is_read_only());
}
void MachineBase::set_activity_observer(Activity::Observer *const observer) {
drive_VIA_.bus_handler().set_activity_observer(observer);
get_drive().set_activity_observer(observer, "Drive", false);
}
// MARK: - 6522 delegate
// MARK: - 6522 delegate.
void MachineBase::mos6522_did_change_interrupt_status(void *) {
// both VIAs are connected to the IRQ line
m6502_.set_irq_line(serial_port_VIA_.get_interrupt_line() || drive_VIA_.get_interrupt_line());
m6502_.set<CPU::MOS6502Mk2::Line::IRQ>(serial_port_VIA_.get_interrupt_line() || drive_VIA_.get_interrupt_line());
}
// MARK: - Disk drive
// MARK: - Disk drive.
void MachineBase::process_input_bit(int value) {
void MachineBase::process_input_bit(const int value) {
shift_register_ = (shift_register_ << 1) | value;
if((shift_register_ & 0x3ff) == 0x3ff) {
drive_VIA_port_handler_.set_sync_detected(true);
@@ -136,18 +147,40 @@ void MachineBase::process_input_bit(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_.get_should_set_overflow()) {
m6502_.set_overflow_line(true);
if(set_cpu_overflow_) {
m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(true);
}
} else {
m6502_.set<CPU::MOS6502Mk2::Line::Overflow>(false);
}
else m6502_.set_overflow_line(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
@@ -160,11 +193,36 @@ 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>
uint8_t SerialPortVIA::get_port_input() const {
if(port) return port_b_;
if(port) {
return port_b_;
}
return 0xff;
}
@@ -184,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;
@@ -202,14 +264,17 @@ 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"
serial_port_->set_output(::Commodore::Serial::Line::Data,
Serial::LineLevel(!data_level_output_ && (attention_level_input_ != attention_acknowledge_level_)));
// "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_))
);
}
// MARK: - DriveVIA
void DriveVIA::set_delegate(Delegate *delegate) {
void DriveVIA::set_delegate(Delegate *const delegate) {
delegate_ = delegate;
}
@@ -220,56 +285,80 @@ uint8_t DriveVIA::get_port_input() const {
}
void DriveVIA::set_sync_detected(const bool sync_detected) {
port_b_ = (port_b_ & 0x7f) | (sync_detected ? 0x00 : 0x80);
port_b_ = (port_b_ & ~0x80) | (sync_detected ? 0x00 : 0x80);
}
void DriveVIA::set_data_input(uint8_t value) {
void DriveVIA::set_is_read_only(const bool is_read_only) {
port_b_ = (port_b_ & ~0x10) | (is_read_only ? 0x00 : 0x10);
}
void DriveVIA::set_data_input(const uint8_t value) {
port_a_ = value;
}
bool DriveVIA::get_should_set_overflow() {
return should_set_overflow_;
}
bool DriveVIA::get_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_);
}
}
}
}
template <MOS::MOS6522::Port port>
void DriveVIA::set_port_output(const uint8_t value, uint8_t) {
if(port) {
if(previous_port_b_output_ != value) {
// Record drive motor state.
drive_motor_ = value&4;
// Check for a head step.
const int step_difference = ((value&3) - (previous_port_b_output_&3))&3;
if(step_difference) {
if(delegate_) delegate_->drive_via_did_step_head(*this, (step_difference == 1) ? 1 : -1);
if(port == MOS::MOS6522::Port::B && line == MOS::MOS6522::Line::Two) {
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_);
}
// Check for a change in density.
const int density_difference = (previous_port_b_output_^value) & (3 << 5);
if(density_difference && delegate_) {
delegate_->drive_via_did_set_data_density(*this, (value >> 5)&3);
}
// Post the LED status.
if(observer_) observer_->set_led_status("Drive", value&8);
previous_port_b_output_ = value;
}
}
}
void DriveVIA::set_activity_observer(Activity::Observer *observer) {
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.
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;
if(step_difference && delegate_) {
delegate_->drive_via_did_step_head(*this, (step_difference == 1) ? 1 : -1);
}
// Check for a change in density.
const int density_difference = (previous_port_b_output_^value) & (3 << 5);
if(density_difference && delegate_) {
delegate_->drive_via_did_set_data_density(*this, (value >> 5)&3);
}
// Post the LED status.
if(observer_) {
observer_->set_led_status("Drive", value&8);
}
previous_port_b_output_ = value;
}
}
template <>
void DriveVIA::set_port_output<MOS::MOS6522::Port::A>(const uint8_t value, uint8_t) {
if(delegate_) {
delegate_->drive_via_set_to_shifter_output(*this, value);
}
}
void DriveVIA::set_activity_observer(Activity::Observer *const observer) {
observer_ = observer;
if(observer) {
observer->register_led("Drive");
@@ -277,9 +366,9 @@ void DriveVIA::set_activity_observer(Activity::Observer *observer) {
}
}
// MARK: - SerialPort
// MARK: - SerialPort.
void SerialPort::set_input(Serial::Line line, Serial::LineLevel level) {
void SerialPort::set_input(const Serial::Line line, const Serial::LineLevel level) {
serial_port_via_->set_serial_line_state(line, bool(level), *via_);
}

View File

@@ -8,7 +8,7 @@
#pragma once
#include "Processors/6502/6502.hpp"
#include "Processors/6502Mk2/6502Mk2.hpp"
#include "Components/6522/6522.hpp"
#include "Machines/Commodore/SerialBus.hpp"
@@ -65,21 +65,27 @@ private:
It is wired up such that Port B contains:
Bits 0/1: head step direction
Bit 2: motor control
Bit 3: LED control (TODO)
Bit 3: LED control
Bit 4: write protect photocell status (TODO)
Bits 5/6: read/write density
Bit 7: 0 if sync marks are currently being detected, 1 otherwise.
... and Port A contains the byte most recently read from the disk or the byte next to write to the disk, depending on data direction.
It is implied that CA2 might be used to set processor overflow, CA1 a strobe for data input, and one of the CBs being definitive on
whether the disk head is being told to read or write, but it's unclear and I've yet to investigate. So, TODO.
Elsewhere:
* CA2 might is used to set processor overflow;
* CA1 a strobe for data input; and
* CB2 indicates read/write mode; 1 = read, 0 = write.
*/
class DriveVIA: public MOS::MOS6522::IRQDelegatePortHandler {
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 *);
@@ -88,8 +94,7 @@ public:
void set_sync_detected(bool);
void set_data_input(uint8_t);
bool get_should_set_overflow();
bool get_motor_enabled();
void set_is_read_only(bool);
template <MOS::MOS6522::Port, MOS::MOS6522::Line>
void set_control_line_output(bool value);
@@ -101,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;
@@ -122,7 +130,6 @@ private:
};
class MachineBase:
public CPU::MOS6502::BusHandler,
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
public DriveVIA::Delegate,
public Storage::Disk::Controller {
@@ -134,17 +141,27 @@ public:
void set_activity_observer(Activity::Observer *);
// to satisfy CPU::MOS6502::Processor
Cycles perform_bus_operation(CPU::MOS6502::BusOperation, uint16_t address, uint8_t *value);
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
Cycles perform(const AddressT, CPU::MOS6502Mk2::data_t<operation>);
protected:
// to satisfy MOS::MOS6522::Delegate
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;
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, MachineBase, false> m6502_;
struct M6502Traits {
static constexpr auto uses_ready_line = false;
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::AnyCycle;
using BusHandlerT = MachineBase;
};
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
uint8_t ram_[0x800];
uint8_t rom_[0x4000];
@@ -156,9 +173,15 @@ protected:
MOS::MOS6522::MOS6522<DriveVIA> drive_VIA_;
MOS::MOS6522::MOS6522<SerialPortVIA> serial_port_VIA_;
bool set_cpu_overflow_ = false;
int shift_register_ = 0, bit_window_offset_;
void process_input_bit(int value) override;
void process_index_hole() override;
void process_write_completed() override;
void is_writing_final_bit() override;
uint8_t port_a_output_ = 0xff;
void serialise_shift_output();
};
}

View File

@@ -16,7 +16,7 @@
#include "Machines/MachineTypes.hpp"
#include "Machines/Utility/MemoryFuzzer.hpp"
#include "Processors/6502/6502.hpp"
#include "Processors/6502Mk2/6502Mk2.hpp"
#include "Analyser/Static/Commodore/Target.hpp"
#include "Outputs/Log.hpp"
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
@@ -236,20 +236,17 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
// HACK. NOCOMMIT.
// int pulse_num_ = 0;
Cycles perform_bus_operation(
const CPU::MOS6502::BusOperation operation,
const uint16_t address,
uint8_t *const value
) {
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
Cycles perform(const AddressT address, CPU::MOS6502Mk2::data_t<operation> value) {
// Determine from the TED video subsystem the length of this clock cycle as perceived by the 6502,
// relative to the master clock.
const auto length = video_.cycle_length(operation == CPU::MOS6502::BusOperation::Ready);
const auto length = video_.cycle_length(operation == CPU::MOS6502Mk2::BusOperation::Ready);
// Update other subsystems.
advance_timers_and_tape(length);
@@ -265,7 +262,7 @@ public:
time_since_audio_update_ += length;
}
if(operation == CPU::MOS6502::BusOperation::Ready) {
if(operation == CPU::MOS6502Mk2::BusOperation::Ready) {
return length;
}
@@ -282,17 +279,17 @@ public:
// b1 = serial clock out and cassette write;
// b0 = serial data out.
if(is_read(operation)) {
if constexpr (is_read(operation)) {
if(!address) {
*value = io_direction_;
value = io_direction_;
} else {
*value = io_input();
value = io_input();
}
} else {
if(!address) {
io_direction_ = *value;
io_direction_ = value;
} else {
io_output_ = *value;
io_output_ = value;
}
const auto output = io_output_ | ~io_direction_;
@@ -323,36 +320,36 @@ public:
// );
// }
if(
use_fast_tape_hack_ &&
operation == CPU::MOS6502Esque::BusOperation::ReadOpcode &&
(
(use_hle && address == 0xe5fd) ||
address == 0xe68b ||
address == 0xe68d
)
) {
// ++pulse_num_;
if(use_hle) {
read_dipole();
}
if constexpr (is_read(operation)) {
if(
use_fast_tape_hack_ &&
operation == CPU::MOS6502Mk2::BusOperation::ReadOpcode &&
(
(use_hle && address == 0xe5fd) ||
address == 0xe68b ||
address == 0xe68d
)
) {
// ++pulse_num_;
if(use_hle) {
read_dipole();
}
// using Flag = CPU::MOS6502::Flag;
// using Register = CPU::MOS6502::Register;
// const auto flags = m6502_.value_of(Register::Flags);
// printf("to %lld: %c%c%c\n",
// tape_player_->serialiser()->offset(),
// flags & Flag::Sign ? 'n' : '-',
// flags & Flag::Overflow ? 'v' : '-',
// flags & Flag::Carry ? 'c' : '-'
// );
*value = 0x60;
} else {
if(is_read(operation)) {
*value = map_.read(address);
// using Flag = CPU::MOS6502::Flag;
// using Register = CPU::MOS6502::Register;
// const auto flags = m6502_.value_of(Register::Flags);
// printf("to %lld: %c%c%c\n",
// tape_player_->serialiser()->offset(),
// flags & Flag::Sign ? 'n' : '-',
// flags & Flag::Overflow ? 'v' : '-',
// flags & Flag::Carry ? 'c' : '-'
// );
value = 0x60;
} else {
map_.write(address) = *value;
value = map_.read(address);
}
} else {
map_.write(address) = value;
}
@@ -393,12 +390,12 @@ public:
// ram_[0x90] = 0;
// ram_[0x93] = 0;
//
// *value = 0x0c; // NOP abs.
// value = 0x0c; // NOP abs.
// }
// }
} else if(address < 0xff00) {
// Miscellaneous hardware. All TODO.
if(is_read(operation)) {
if constexpr (is_read(operation)) {
switch(address & 0xfff0) {
case 0xfd10:
// 6529 parallel port, about which I know only what I've found in kernel ROM disassemblies.
@@ -406,7 +403,7 @@ public:
// If play button is not currently pressed and this read is immediately followed by
// an AND 4, press it. The kernel will deal with motor control subsequently.
if(!play_button_) {
const uint16_t pc = m6502_.value_of(CPU::MOS6502::Register::ProgramCounter);
const uint16_t pc = m6502_.registers().pc.full;
const uint8_t next[] = {
map_.read(pc+0),
map_.read(pc+1),
@@ -422,22 +419,23 @@ public:
}
}
*value = 0xff ^ (play_button_ ? 0x4 :0x0);
value = 0xff ^ (play_button_ ? 0x4 :0x0);
break;
case 0xfdd0:
case 0xfdf0:
*value = uint8_t(address >> 8);
value = uint8_t(address >> 8);
break;
default:
value = 0xff;
Logger::info().append("TODO: read @ %04x", address);
break;
}
} else {
switch(address & 0xfff0) {
case 0xfd30:
keyboard_mask_ = *value;
keyboard_mask_ = value;
break;
case 0xfdd0: {
@@ -447,28 +445,28 @@ public:
} break;
default:
Logger::info().append("TODO: write of %02x @ %04x", *value, address);
Logger::info().append("TODO: write of %02x @ %04x", value, address);
break;
}
}
} else {
const auto pc = m6502_.value_of(CPU::MOS6502::Register::ProgramCounter);
const auto pc = m6502_.registers().pc.full;
const bool is_from_rom =
(rom_is_paged_ && pc >= 0x8000) ||
(pc >= 0x400 && pc < 0x500) ||
(pc >= 0x700 && pc < 0x800);
bool is_hit = true;
if(is_read(operation)) {
if constexpr (is_read(operation)) {
switch(address) {
case 0xff00: *value = timers_.read<0>(); break;
case 0xff01: *value = timers_.read<1>(); break;
case 0xff02: *value = timers_.read<2>(); break;
case 0xff03: *value = timers_.read<3>(); break;
case 0xff04: *value = timers_.read<4>(); break;
case 0xff05: *value = timers_.read<5>(); break;
case 0xff06: *value = video_.read<0xff06>(); break;
case 0xff07: *value = video_.read<0xff07>(); break;
case 0xff00: value = timers_.read<0>(); break;
case 0xff01: value = timers_.read<1>(); break;
case 0xff02: value = timers_.read<2>(); break;
case 0xff03: value = timers_.read<3>(); break;
case 0xff04: value = timers_.read<4>(); break;
case 0xff05: value = timers_.read<5>(); break;
case 0xff06: value = video_.read<0xff06>(); break;
case 0xff07: value = video_.read<0xff07>(); break;
case 0xff08: {
const uint8_t keyboard_input =
~(
@@ -487,127 +485,128 @@ public:
((joystick_mask_ & 0x02) ? 0xff : (joystick(0).mask() | 0x40)) &
((joystick_mask_ & 0x04) ? 0xff : (joystick(1).mask() | 0x80));
*value = keyboard_input & joystick_mask;
value = keyboard_input & joystick_mask;
} break;
case 0xff09: *value = interrupts_.status(); break;
case 0xff09: value = interrupts_.status(); break;
case 0xff0a:
*value = interrupts_.mask() | video_.read<0xff0a>() | 0xa0;
value = interrupts_.mask() | video_.read<0xff0a>() | 0xa0;
break;
case 0xff0b: *value = video_.read<0xff0b>(); break;
case 0xff0c: *value = video_.read<0xff0c>(); break;
case 0xff0d: *value = video_.read<0xff0d>(); break;
case 0xff0e: *value = ff0e_; break;
case 0xff0f: *value = ff0f_; break;
case 0xff10: *value = ff10_ | 0xfc; break;
case 0xff11: *value = ff11_; break;
case 0xff12: *value = ff12_ | 0xc0; break;
case 0xff13: *value = ff13_ | (rom_is_paged_ ? 1 : 0); break;
case 0xff14: *value = video_.read<0xff14>(); break;
case 0xff15: *value = video_.read<0xff15>(); break;
case 0xff16: *value = video_.read<0xff16>(); break;
case 0xff17: *value = video_.read<0xff17>(); break;
case 0xff18: *value = video_.read<0xff18>(); break;
case 0xff19: *value = video_.read<0xff19>(); break;
case 0xff1a: *value = video_.read<0xff1a>(); break;
case 0xff1b: *value = video_.read<0xff1b>(); break;
case 0xff1c: *value = video_.read<0xff1c>(); break;
case 0xff1d: *value = video_.read<0xff1d>(); break;
case 0xff1e: *value = video_.read<0xff1e>(); break;
case 0xff1f: *value = video_.read<0xff1f>(); break;
case 0xff0b: value = video_.read<0xff0b>(); break;
case 0xff0c: value = video_.read<0xff0c>(); break;
case 0xff0d: value = video_.read<0xff0d>(); break;
case 0xff0e: value = ff0e_; break;
case 0xff0f: value = ff0f_; break;
case 0xff10: value = ff10_ | 0xfc; break;
case 0xff11: value = ff11_; break;
case 0xff12: value = ff12_ | 0xc0; break;
case 0xff13: value = ff13_ | (rom_is_paged_ ? 1 : 0); break;
case 0xff14: value = video_.read<0xff14>(); break;
case 0xff15: value = video_.read<0xff15>(); break;
case 0xff16: value = video_.read<0xff16>(); break;
case 0xff17: value = video_.read<0xff17>(); break;
case 0xff18: value = video_.read<0xff18>(); break;
case 0xff19: value = video_.read<0xff19>(); break;
case 0xff1a: value = video_.read<0xff1a>(); break;
case 0xff1b: value = video_.read<0xff1b>(); break;
case 0xff1c: value = video_.read<0xff1c>(); break;
case 0xff1d: value = video_.read<0xff1d>(); break;
case 0xff1e: value = video_.read<0xff1e>(); break;
case 0xff1f: value = video_.read<0xff1f>(); break;
case 0xff3e: *value = 0; break;
case 0xff3f: *value = 0; break;
case 0xff3e: value = 0; break;
case 0xff3f: value = 0; break;
default:
Logger::info().append("TODO: TED read at %04x", address);
value = 0xff;
is_hit = false;
}
} else {
switch(address) {
case 0xff00: timers_.write<0>(*value); break;
case 0xff01: timers_.write<1>(*value); break;
case 0xff02: timers_.write<2>(*value); break;
case 0xff03: timers_.write<3>(*value); break;
case 0xff04: timers_.write<4>(*value); break;
case 0xff05: timers_.write<5>(*value); break;
case 0xff06: video_.write<0xff06>(*value); break;
case 0xff00: timers_.write<0>(value); break;
case 0xff01: timers_.write<1>(value); break;
case 0xff02: timers_.write<2>(value); break;
case 0xff03: timers_.write<3>(value); break;
case 0xff04: timers_.write<4>(value); break;
case 0xff05: timers_.write<5>(value); break;
case 0xff06: video_.write<0xff06>(value); break;
case 0xff07:
video_.write<0xff07>(*value);
video_.write<0xff07>(value);
update_audio();
audio_.set_divider(*value);
audio_.set_divider(value);
break;
case 0xff08:
// Observation here: the kernel posts a 0 to this
// address upon completing each keyboard scan cycle,
// once per frame.
if(typer_ && !*value) {
if(typer_ && !value) {
if(!typer_->type_next_character()) {
clear_all_keys();
typer_.reset();
}
}
joystick_mask_ = *value;
joystick_mask_ = value;
break;
case 0xff09:
interrupts_.set_status(*value);
interrupts_.set_status(value);
break;
case 0xff0a:
interrupts_.set_mask(*value);
video_.write<0xff0a>(*value);
interrupts_.set_mask(value);
video_.write<0xff0a>(value);
break;
case 0xff0b: video_.write<0xff0b>(*value); break;
case 0xff0c: video_.write<0xff0c>(*value); break;
case 0xff0d: video_.write<0xff0d>(*value); break;
case 0xff0b: video_.write<0xff0b>(value); break;
case 0xff0c: video_.write<0xff0c>(value); break;
case 0xff0d: video_.write<0xff0d>(value); break;
case 0xff0e:
ff0e_ = *value;
ff0e_ = value;
update_audio();
audio_.set_frequency_low<0>(*value);
audio_.set_frequency_low<0>(value);
break;
case 0xff0f:
ff0f_ = *value;
ff0f_ = value;
update_audio();
audio_.set_frequency_low<1>(*value);
audio_.set_frequency_low<1>(value);
break;
case 0xff10:
ff10_ = *value;
ff10_ = value;
update_audio();
audio_.set_frequency_high<1>(*value);
audio_.set_frequency_high<1>(value);
break;
case 0xff11:
ff11_ = *value;
ff11_ = value;
update_audio();
audio_.set_control(*value);
audio_.set_control(value);
break;
case 0xff12:
ff12_ = *value & 0x3f;
video_.write<0xff12>(*value);
ff12_ = value & 0x3f;
video_.write<0xff12>(value);
if((*value & 4)) {
if((value & 4)) {
page_video_rom();
} else {
page_video_ram();
}
update_audio();
audio_.set_frequency_high<0>(*value);
audio_.set_frequency_high<0>(value);
break;
case 0xff13:
ff13_ = *value & 0xfe;
video_.write<0xff13>(*value);
ff13_ = value & 0xfe;
video_.write<0xff13>(value);
break;
case 0xff14: video_.write<0xff14>(*value); break;
case 0xff15: video_.write<0xff15>(*value); break;
case 0xff16: video_.write<0xff16>(*value); break;
case 0xff17: video_.write<0xff17>(*value); break;
case 0xff18: video_.write<0xff18>(*value); break;
case 0xff19: video_.write<0xff19>(*value); break;
case 0xff1a: video_.write<0xff1a>(*value); break;
case 0xff1b: video_.write<0xff1b>(*value); break;
case 0xff1c: video_.write<0xff1c>(*value); break;
case 0xff1d: video_.write<0xff1d>(*value); break;
case 0xff1e: video_.write<0xff1e>(*value); break;
case 0xff1f: video_.write<0xff1f>(*value); break;
case 0xff14: video_.write<0xff14>(value); break;
case 0xff15: video_.write<0xff15>(value); break;
case 0xff16: video_.write<0xff16>(value); break;
case 0xff17: video_.write<0xff17>(value); break;
case 0xff18: video_.write<0xff18>(value); break;
case 0xff19: video_.write<0xff19>(value); break;
case 0xff1a: video_.write<0xff1a>(value); break;
case 0xff1b: video_.write<0xff1b>(value); break;
case 0xff1c: video_.write<0xff1c>(value); break;
case 0xff1d: video_.write<0xff1d>(value); break;
case 0xff1e: video_.write<0xff1e>(value); break;
case 0xff1f: video_.write<0xff1f>(value); break;
case 0xff3e: page_cpu_rom(); break;
case 0xff3f: page_cpu_ram(); break;
@@ -626,8 +625,12 @@ public:
}
private:
using Processor = CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, true>;
Processor m6502_;
struct M6502Traits {
static constexpr auto uses_ready_line = true;
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::BetweenInstructions;
using BusHandlerT = ConcreteMachine;
};
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
Outputs::Speaker::Speaker *get_speaker() override {
return &speaker_;
@@ -637,11 +640,11 @@ private:
if(c1541_) c1541_->set_activity_observer(observer);
}
void set_irq_line(bool active) override {
m6502_.set_irq_line(active);
void set_irq_line(const bool active) override {
m6502_.template set<CPU::MOS6502Mk2::Line::IRQ>(active);
}
void set_ready_line(bool active) override {
m6502_.set_ready_line(active);
void set_ready_line(const bool active) override {
m6502_.template set<CPU::MOS6502Mk2::Line::Ready>(active);
}
void page_video_rom() {
@@ -788,26 +791,19 @@ private:
// TODO: substantially simplify the below; at the minute it's a
// literal transcription of the original as a simple first step.
void read_dipole() {
using Register = CPU::MOS6502::Register;
using Flag = CPU::MOS6502::Flag;
using Flag = CPU::MOS6502Mk2::Flag;
//
// Get registers now and ensure they'll be written back at function exit.
//
CPU::MOS6502Esque::LazyFlags flags(uint8_t(m6502_.value_of(Register::Flags)));
uint8_t x, y, a;
uint8_t s = uint8_t(m6502_.value_of(Register::StackPointer));
auto registers = m6502_.registers();
struct ScopeGuard {
ScopeGuard(std::function<void(void)> at_exit) : at_exit_(at_exit) {}
~ScopeGuard() { at_exit_(); }
private:
std::function<void(void)> at_exit_;
} registers([&] {
m6502_.set_value_of(Register::Flags, flags.get());
m6502_.set_value_of(Register::A, a);
m6502_.set_value_of(Register::X, x);
m6502_.set_value_of(Register::Y, y);
m6502_.set_value_of(Register::StackPointer, s);
} store_registers([&] {
m6502_.set_registers(registers);
});
//
@@ -822,38 +818,38 @@ private:
// 6502 pseudo-ops.
//
const auto ldabs = [&] (uint8_t &target, const uint16_t address) {
flags.set_nz(target = map_.read(address));
registers.flags.set_per<Flag::NegativeZero>(target = map_.read(address));
};
const auto ldimm = [&] (uint8_t &target, const uint8_t value) {
flags.set_nz(target = value);
registers.flags.set_per<Flag::NegativeZero>(target = value);
};
const auto pha = [&] () {
map_.write(0x100 + s) = a;
--s;
map_.write(0x100 + registers.s) = registers.a;
--registers.s;
};
const auto pla = [&] () {
++s;
a = map_.read(0x100 + s);
++registers.s;
registers.a = map_.read(0x100 + registers.s);
};
const auto bit = [&] (const uint8_t value) {
flags.zero_result = a & value;
flags.negative_result = value;
flags.overflow = value & CPU::MOS6502Esque::Flag::Overflow;
registers.flags.set_per<Flag::Zero>(registers.a & value);
registers.flags.set_per<Flag::Negative>(value);
registers.flags.set_per<Flag::Overflow>(value);
};
const auto cmp = [&] (const uint8_t value) {
const uint16_t temp16 = a - value;
flags.set_nz(uint8_t(temp16));
flags.carry = ((~temp16) >> 8)&1;
const uint16_t temp16 = registers.a - value;
registers.flags.set_per<Flag::NegativeZero>(uint8_t(temp16));
registers.flags.set_per<Flag::Carry>(((~temp16) >> 8)&1);
};
const auto andimm = [&] (const uint8_t value) {
a &= value;
flags.set_nz(a);
registers.a &= value;
registers.flags.set_per<Flag::NegativeZero>(registers.a);
};
const auto ne = [&]() -> bool {
return flags.zero_result;
return !registers.flags.get<Flag::Zero>();
};
const auto eq = [&]() -> bool {
return !flags.zero_result;
return registers.flags.get<Flag::Zero>();
};
//
@@ -862,7 +858,7 @@ private:
const auto dipok = [&] {
// clc ; everything's fine
// rts
flags.carry = 0;
registers.flags.set_per<Flag::Carry>(0);
};
const auto rshort = [&] {
// bit tshrtd ; got a short
@@ -878,7 +874,7 @@ private:
const auto rderr1 = [&] {
// sec ; i'm confused
// rts
flags.carry = Flag::Carry;
registers.flags.set_per<Flag::Carry>(Flag::Carry);
};
//
@@ -891,8 +887,8 @@ private:
//rddipl
// ldx dsamp1 ; setup x,y with 1st sample point
// ldy dsamp1+1
ldabs(x, dsamp1);
ldabs(y, dsamp1 + 1);
ldabs(registers.x, dsamp1);
ldabs(registers.y, dsamp1 + 1);
advance_cycles(8);
//badeg1
@@ -901,9 +897,9 @@ private:
// pha
// lda dsamp2
// pha
ldabs(a, dsamp2 + 1);
ldabs(registers.a, dsamp2 + 1);
pha();
ldabs(a, dsamp2);
ldabs(registers.a, dsamp2);
pha();
advance_cycles(14);
@@ -911,7 +907,7 @@ private:
//rwtl ; wait till rd line is high
// bit port [= $0001]
// beq rwtl ; !ls!
ldimm(a, 0x10);
ldimm(registers.a, 0x10);
advance_cycles(2);
do {
bit(io_input());
@@ -933,8 +929,8 @@ private:
// stx timr2l
// sty timr2h
timers_.write<2>(x);
timers_.write<3>(y);
timers_.write<2>(registers.x);
timers_.write<3>(registers.y);
advance_cycles(8);
@@ -945,9 +941,9 @@ private:
// pla
// sta timr3h ;go! ...tb
pla();
timers_.write<4>(a);
timers_.write<4>(registers.a);
pla();
timers_.write<5>(a);
timers_.write<5>(registers.a);
advance_cycles(14);
@@ -955,8 +951,8 @@ private:
//
// lda #$50 ; clr ta,tb
// sta tedirq
ldimm(a, 0x50);
interrupts_.set_status(a);
ldimm(registers.a, 0x50);
interrupts_.set_status(registers.a);
advance_cycles(6);
@@ -969,7 +965,7 @@ private:
// and #$10 ; a look at that edge again
// bne badeg1 ; woa! got a bad edge trigger !ls!
do {
ldimm(a, io_input());
ldimm(registers.a, io_input());
cmp(io_input());
if(advance_cycles(9)) {
return;
@@ -992,7 +988,7 @@ private:
// lda #$10
//wata ; wait for ta to timeout
ldimm(a, 0x10);
ldimm(registers.a, 0x10);
advance_cycles(3);
do {
// bit port ; kuldge, kludge, kludge !!! <<><>>
@@ -1020,7 +1016,7 @@ private:
do {
// lda port
// cmp port
ldimm(a, io_input());
ldimm(registers.a, io_input());
cmp(io_input());
if(advance_cycles(9)) {
@@ -1048,7 +1044,7 @@ private:
//
//; wait for tb to timeout
//; now do the dipole sample #2
ldimm(a, 0x40);
ldimm(registers.a, 0x40);
advance_cycles(3);
do {
bit(interrupts_.status());
@@ -1063,7 +1059,7 @@ private:
// cmp port
// bne casdb3
do {
ldimm(a, io_input());
ldimm(registers.a, io_input());
cmp(io_input());
if(advance_cycles(9)) {
return;
@@ -1084,10 +1080,10 @@ private:
// sta timr2l
// lda zcell+1
// sta timr2h
ldabs(a, zcell);
timers_.write<2>(a);
ldabs(a, zcell + 1);
timers_.write<3>(y);
ldabs(registers.a, zcell);
timers_.write<2>(registers.a);
ldabs(registers.a, zcell + 1);
timers_.write<3>(registers.y);
advance_cycles(16);
@@ -1096,9 +1092,9 @@ private:
// lda #$10
// sta tedirq ; verify +180 half of word dipole
// lda #$10
ldimm(a, 0x10);
interrupts_.set_status(a);
ldimm(a, 0x10);
ldimm(registers.a, 0x10);
interrupts_.set_status(registers.a);
ldimm(registers.a, 0x10);
advance_cycles(8);
//wata2
@@ -1116,7 +1112,7 @@ private:
// cmp port
// bne casdb4
do {
ldimm(a, io_input());
ldimm(registers.a, io_input());
cmp(io_input());
if(advance_cycles(9)) {
return;

View File

@@ -13,7 +13,7 @@
#include "Activity/Source.hpp"
#include "Machines/MachineTypes.hpp"
#include "Processors/6502/6502.hpp"
#include "Processors/6502Mk2/6502Mk2.hpp"
#include "Components/6560/6560.hpp"
#include "Components/6522/6522.hpp"
@@ -72,26 +72,30 @@ public:
// Port A provides information about the presence or absence of a tape, and parts of
// the joystick and serial port state, both of which have been statefully collected
// into port_a_.
if(!port) {
if constexpr (port == MOS::MOS6522::Port::A) {
return port_a_ | (tape_->has_tape() ? 0x00 : 0x40);
}
return 0xff;
}
/// Receives announcements of control line output change from the 6522.
template <MOS::MOS6522::Port port, MOS::MOS6522::Line line> void set_control_line_output(const bool value) {
// The CA2 output is used to control the tape motor.
if(port == MOS::MOS6522::Port::A && line == MOS::MOS6522::Line::Two) {
template <MOS::MOS6522::Port port, MOS::MOS6522::Line line>
void set_control_line_output(const bool value) {
// CA2: control the tape motor.
if constexpr (port == MOS::MOS6522::Port::A && line == MOS::MOS6522::Line::Two) {
tape_->set_motor_control(!value);
}
}
/// Receives announcements of changes in the serial bus connected to the serial port and propagates them into Port A.
void set_serial_line_state(Commodore::Serial::Line line, const bool value) {
void set_serial_line_state(const Commodore::Serial::Line line, const bool value) {
const auto set = [&](const uint8_t bit) {
port_a_ = (port_a_ & ~bit) | (value ? bit : 0x00);
};
switch(line) {
default: break;
case ::Commodore::Serial::Line::Data: port_a_ = (port_a_ & ~0x02) | (value ? 0x02 : 0x00); break;
case ::Commodore::Serial::Line::Clock: port_a_ = (port_a_ & ~0x01) | (value ? 0x01 : 0x00); break;
case ::Commodore::Serial::Line::Data: set(0x02); break;
case ::Commodore::Serial::Line::Clock: set(0x01); break;
}
}
@@ -146,7 +150,7 @@ public:
/// Sets all keys as unpressed.
void clear_all_keys() {
memset(columns_, 0xff, sizeof(columns_));
std::fill(std::begin(columns_), std::end(columns_), 0xff);
}
/// Called by the 6522 to get input. Reads the keyboard on Port A, returns a small amount of joystick state on Port B.
@@ -230,8 +234,6 @@ struct Vic6560BusHandler {
// It is assumed that these pointers have been filled in by the machine.
const uint8_t *video_memory_map[16]{}; // Segments video memory into 1kb portions.
const uint8_t *colour_memory{}; // Colour memory must be contiguous.
// TODO: make the above const.
};
/*!
@@ -280,7 +282,6 @@ class ConcreteMachine:
public MachineTypes::MappedKeyboardMachine,
public MachineTypes::JoystickMachine,
public Configurable::Device,
public CPU::MOS6502::BusHandler,
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
public Utility::TypeRecipient<CharacterMapper>,
public Storage::Tape::BinaryTapePlayer::Delegate,
@@ -455,54 +456,50 @@ public:
}
void set_key_state(const uint16_t key, const bool is_pressed) final {
if(key < 0xfff0) {
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;
}
}
void clear_all_keys() final {
keyboard_via_port_handler_.clear_all_keys();
set_key_state(KeyRestore, false);
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return joysticks_;
}
// to satisfy CPU::MOS6502::Processor
forceinline Cycles perform_bus_operation(
const CPU::MOS6502::BusOperation operation,
const uint16_t address,
uint8_t *const value
) {
template <CPU::MOS6502Mk2::BusOperation operation, typename AddressT>
Cycles perform(const AddressT address, CPU::MOS6502Mk2::data_t<operation> value) {
// Tun the phase-1 part of this cycle, in which the VIC accesses memory.
cycles_since_mos6560_update_++;
// Run the phase-2 part of the cycle, which is whatever the 6502 said it should be.
const bool is_from_rom = m6502_.value_of(CPU::MOS6502::Register::ProgramCounter) > 0x8000;
if(is_read(operation)) {
const auto is_from_rom = [&]() {
return m6502_.registers().pc.full > 0x8000;
};
if constexpr (is_read(operation)) {
const auto page = processor_read_memory_map_[address >> 10];
uint8_t result;
if(!page) {
if(!is_from_rom) confidence_.add_miss();
if(!is_from_rom()) confidence_.add_miss();
result = 0xff;
} else {
result = processor_read_memory_map_[address >> 10][address & 0x3ff];
@@ -515,7 +512,7 @@ public:
if(address & 0x10) result &= user_port_via_.read(address);
if(address & 0x20) result &= keyboard_via_.read(address);
if(!is_from_rom) {
if(!is_from_rom()) {
if((address & 0x100) && !(address & 0x30)) {
confidence_.add_miss();
} else {
@@ -523,10 +520,10 @@ public:
}
}
}
*value = result;
value = result;
// Consider applying the fast tape hack.
if(use_fast_tape_hack_ && operation == CPU::MOS6502::BusOperation::ReadOpcode) {
if(use_fast_tape_hack_ && operation == CPU::MOS6502Mk2::BusOperation::ReadOpcode) {
if(address == 0xf7b2) {
// Address 0xf7b2 contains a JSR to 0xf8c0 ('RDTPBLKS') that will fill the tape buffer with the
// next header. Skip that via a three-byte NOP and fill in the next header programmatically.
@@ -551,10 +548,10 @@ public:
ram_[0x90] = 0;
ram_[0x93] = 0;
*value = 0x0c; // i.e. NOP abs, to swallow the entire JSR
value = 0x0c; // i.e. NOP abs, to swallow the entire JSR
} else if(address == 0xf90b) {
const auto x = uint8_t(m6502_.value_of(CPU::MOS6502::Register::X));
if(x == 0xe) {
auto registers = m6502_.registers();
if(registers.x == 0xe) {
Storage::Tape::Commodore::Parser parser(TargetPlatform::Vic20);
const auto tape_position = tape_->serialiser()->offset();
const std::unique_ptr<Storage::Tape::Commodore::Data> data = parser.get_next_data(*tape_->serialiser());
@@ -576,14 +573,13 @@ public:
// set tape status, carry and flag
ram_[0x90] |= 0x40;
uint8_t flags = uint8_t(m6502_.value_of(CPU::MOS6502::Register::Flags));
flags &= ~uint8_t((CPU::MOS6502::Flag::Carry | CPU::MOS6502::Flag::Interrupt));
m6502_.set_value_of(CPU::MOS6502::Register::Flags, flags);
registers.flags.set_per<CPU::MOS6502Mk2::Flag::Carry>(0);
registers.flags.set_per<CPU::MOS6502Mk2::Flag::Interrupt>(0);
// to ensure that execution proceeds to 0xfccf, pretend a NOP was here and
// ensure that the PC leaps to 0xfccf
m6502_.set_value_of(CPU::MOS6502::Register::ProgramCounter, 0xfccf);
*value = 0xea; // i.e. NOP implied
registers.pc.full = 0xfccf;
value = 0xea; // i.e. NOP implied
hold_tape_ = true;
Logger::info().append("Found data");
} else {
@@ -592,27 +588,28 @@ public:
Logger::info().append("Didn't find data");
}
}
m6502_.set_registers(registers);
}
}
} else {
uint8_t *const ram = processor_write_memory_map_[address >> 10];
if(ram) {
update_video();
ram[address & 0x3ff] = *value;
ram[address & 0x3ff] = value;
}
// Anything between 0x9000 and 0x9400 is the IO area.
if((address&0xfc00) == 0x9000) {
// The VIC is selected by bit 8 = 0
if(!(address&0x100)) {
update_video();
mos6560_.write(address, *value);
mos6560_.write(address, value);
}
// The first VIA is selected by bit 4 = 1.
if(address & 0x10) user_port_via_.write(address, *value);
if(address & 0x10) user_port_via_.write(address, value);
// The second VIA is selected by bit 5 = 1.
if(address & 0x20) keyboard_via_.write(address, *value);
if(address & 0x20) keyboard_via_.write(address, value);
if(!is_from_rom) {
if(!is_from_rom()) {
if((address & 0x100) && !(address & 0x30)) {
confidence_.add_miss();
} else {
@@ -620,13 +617,13 @@ public:
}
}
} else if(!ram) {
if(!is_from_rom) confidence_.add_miss();
if(!is_from_rom()) confidence_.add_miss();
}
}
user_port_via_.run_for(Cycles(1));
keyboard_via_.run_for(Cycles(1));
if(typer_ && address == 0xeb1e && operation == CPU::MOS6502::BusOperation::ReadOpcode) {
if(typer_ && address == 0xeb1e && operation == CPU::MOS6502Mk2::BusOperation::ReadOpcode) {
if(!typer_->type_next_character()) {
clear_all_keys();
typer_.reset();
@@ -672,8 +669,8 @@ public:
}
void mos6522_did_change_interrupt_status(void *) final {
m6502_.set_nmi_line(user_port_via_.get_interrupt_line());
m6502_.set_irq_line(keyboard_via_.get_interrupt_line());
m6502_.template set<CPU::MOS6502Mk2::Line::NMI>(user_port_via_.get_interrupt_line());
m6502_.template set<CPU::MOS6502Mk2::Line::IRQ>(keyboard_via_.get_interrupt_line());
}
void type_string(const std::string &string) final {
@@ -718,10 +715,16 @@ public:
}
private:
struct M6502Traits {
static constexpr auto uses_ready_line = false;
static constexpr auto pause_precision = CPU::MOS6502Mk2::PausePrecision::BetweenInstructions;
using BusHandlerT = ConcreteMachine;
};
CPU::MOS6502Mk2::Processor<CPU::MOS6502Mk2::Model::M6502, M6502Traits> m6502_;
void update_video() {
mos6560_.run_for(cycles_since_mos6560_update_.flush<Cycles>());
}
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
std::vector<uint8_t> character_rom_;
std::vector<uint8_t> basic_rom_;
@@ -745,12 +748,22 @@ private:
++address;
}
}
void write_to_map(const uint8_t **const map, const uint8_t *area, uint16_t address, size_t length) {
void write_to_map(
const uint8_t **const map,
const uint8_t *const area,
const uint16_t address,
const size_t length
) {
write_to_map([&](const uint16_t address, const size_t offset) {
map[address] = &area[offset];
}, address, length);
}
void write_to_map(uint8_t **const map, uint8_t *area, uint16_t address, size_t length) {
void write_to_map(
uint8_t **const map,
uint8_t *const area,
const uint16_t address,
const size_t length
) {
write_to_map([&](const uint16_t address, const size_t offset) {
map[address] = &area[offset];
}, address, length);

View File

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

View File

@@ -10,6 +10,7 @@
#include "Dave.hpp"
#include "EXDos.hpp"
#include "HostFSHandler.hpp"
#include "Keyboard.hpp"
#include "Nick.hpp"
@@ -23,6 +24,8 @@
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "Processors/Z80/Z80.hpp"
#include <unordered_set>
namespace {
using Logger = Log::Logger<Log::Source::Enterprise>;
}
@@ -72,6 +75,7 @@ template <bool has_disk_controller, bool is_6mhz> class ConcreteMachine:
public Activity::Source,
public Configurable::Device,
public CPU::Z80::BusHandler,
public HostFSHandler::MemoryAccessor,
public Machine,
public MachineTypes::AudioProducer,
public MachineTypes::MappedKeyboardMachine,
@@ -104,7 +108,8 @@ public:
z80_(*this),
nick_(ram_.end() - 65536),
dave_audio_(audio_queue_),
speaker_(dave_audio_) {
speaker_(dave_audio_),
host_fs_(*this) {
// Request a clock of 4Mhz; this'll be mapped upwards for Nick and downwards for Dave elsewhere.
set_clock_rate(clock_rate);
@@ -228,6 +233,14 @@ public:
memcpy(exdos_rom_.data(), exdos->second.data(), std::min(exdos_rom_.size(), exdos->second.size()));
}
// Possibly install the host FS ROM.
host_fs_rom_.fill(0xff);
if(!target.media.file_bundles.empty()) {
const auto rom = host_fs_.rom();
std::copy(rom.begin(), rom.end(), host_fs_rom_.begin());
find_host_fs_hooks();
}
// Seed key state.
clear_all_keys();
@@ -257,7 +270,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
// MARK: - Z80::BusHandler.
@@ -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();

View File

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

View File

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

View File

@@ -0,0 +1,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
};
}

View File

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

View File

@@ -360,7 +360,7 @@ public:
}
~ConcreteMachine() {
speaker_.audio_queue.flush();
speaker_.audio_queue.lock_flush();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {

View File

@@ -178,7 +178,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
ChangeEffect effect_for_file_did_change(const std::string &) const final {

View File

@@ -406,7 +406,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
void set_key_state(uint16_t key, bool is_pressed) final {

View File

@@ -817,7 +817,7 @@ public:
}
~ConcreteMachine() {
speaker_.queue.flush();
speaker_.queue.lock_flush();
}
// MARK: - TimedMachine.

View File

@@ -114,7 +114,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {

View File

@@ -213,7 +213,7 @@ public:
}
~ConcreteMachine() {
audio_queue_.flush();
audio_queue_.lock_flush();
}
static constexpr unsigned int clock_rate() {

View File

@@ -24,8 +24,9 @@ struct SizedInt {
constexpr SizedInt(const IntT start_value) noexcept : counter_(start_value & Mask) {}
SizedInt() = default;
template <int begin = 0>
IntT get() const {
return counter_;
return counter_ >> begin;
}
SizedInt operator +(const SizedInt offset) const { return SizedInt<bits>(counter_ + offset.counter_); }

View File

@@ -753,6 +753,10 @@
4B92E26B234AE35100CD6D1B /* MFP68901.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B92E268234AE35000CD6D1B /* MFP68901.cpp */; };
4B92EACA1B7C112B00246143 /* 6502TimingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92EAC91B7C112B00246143 /* 6502TimingTests.swift */; };
4B9378E422A199C600973513 /* Audio.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B9378E222A199C600973513 /* Audio.cpp */; };
4B96DECC2EBEE7D100505298 /* SID.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96DECA2EBEE7D100505298 /* SID.cpp */; };
4B96DECD2EBEE7D100505298 /* SID.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96DECA2EBEE7D100505298 /* SID.cpp */; };
4B96DECE2EBEE7D100505298 /* SID.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96DECA2EBEE7D100505298 /* SID.cpp */; };
4B96DED32EC3ECDA00505298 /* SIDTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B96DED22EC3ECDA00505298 /* SIDTests.mm */; };
4B96F7CE263E33B10092AEE1 /* DSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96F7CC263E33B10092AEE1 /* DSK.cpp */; };
4B96F7CF263E33B10092AEE1 /* DSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96F7CC263E33B10092AEE1 /* DSK.cpp */; };
4B98A05E1FFAD3F600ADF63B /* CSROMFetcher.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B98A05D1FFAD3F600ADF63B /* CSROMFetcher.mm */; };
@@ -1144,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 */; };
@@ -1952,6 +1962,10 @@
4B95FA9C1F11893B0008E395 /* ZX8081Controller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZX8081Controller.swift; sourceTree = "<group>"; };
4B961408222760E0001A7BF2 /* Screenshot.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Screenshot.hpp; sourceTree = "<group>"; };
4B96DEC32EBEA88C00505298 /* TubeProcessor.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = TubeProcessor.hpp; sourceTree = "<group>"; };
4B96DEC92EBEE7D100505298 /* SID.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SID.hpp; sourceTree = "<group>"; };
4B96DECA2EBEE7D100505298 /* SID.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SID.cpp; sourceTree = "<group>"; };
4B96DED22EC3ECDA00505298 /* SIDTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SIDTests.mm; sourceTree = "<group>"; };
4B96DED42EC53BC300505298 /* BiquadFilter.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = BiquadFilter.hpp; sourceTree = "<group>"; };
4B96F7CB263E30B00092AEE1 /* RawSectorDump.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RawSectorDump.hpp; sourceTree = "<group>"; };
4B96F7CC263E33B10092AEE1 /* DSK.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DSK.cpp; sourceTree = "<group>"; };
4B96F7CD263E33B10092AEE1 /* DSK.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DSK.hpp; sourceTree = "<group>"; };
@@ -2417,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>"; };
@@ -2692,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 */,
);
@@ -3017,6 +3039,7 @@
4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */,
4BC76E681C98E31700E6EF73 /* FIRFilter.hpp */,
4B24095A1C45DF85004DA684 /* Stepper.hpp */,
4B96DED42EC53BC300505298 /* BiquadFilter.hpp */,
);
name = SignalProcessing;
path = ../../SignalProcessing;
@@ -3578,6 +3601,7 @@
4BEE0A691D72496600532C7B /* Cartridge */,
4B8805F81DCFF6CD003085B1 /* Data */,
4BAB62AA1D3272D200DF5BA0 /* Disk */,
4BCF1ACE2ECE759000109999 /* FileBundle */,
4B6AAEA1230E3E1D0078E864 /* MassStorage */,
4B8DD3832634D37E00B3C866 /* State */,
4B69FB3A1C4D908A00B5F0AA /* Tape */,
@@ -4354,6 +4378,15 @@
path = 68901;
sourceTree = "<group>";
};
4B96DECB2EBEE7D100505298 /* SID */ = {
isa = PBXGroup;
children = (
4B96DEC92EBEE7D100505298 /* SID.hpp */,
4B96DECA2EBEE7D100505298 /* SID.cpp */,
);
path = SID;
sourceTree = "<group>";
};
4B9F11C72272375400701480 /* QL Startup */ = {
isa = PBXGroup;
children = (
@@ -4872,6 +4905,7 @@
4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */,
4B3F76B825A1635300178AEC /* PowerPCDecoderTests.mm */,
4BE76CF822641ED300ACD6FA /* QLTests.mm */,
4B96DED22EC3ECDA00505298 /* SIDTests.mm */,
4B8DD3672633B2D400B3C866 /* SpectrumVideoContentionTests.mm */,
4B2AF8681E513FC20027EE29 /* TIATests.mm */,
4B1D08051E0F7A1100763741 /* TimeTests.mm */,
@@ -5168,6 +5202,7 @@
4BF0BC6E2973318E00CCA2B5 /* RP5C01 */,
4B8855A42E84D51B00E251DD /* SAA5050 */,
4B0ACBFF237756EC008902D0 /* Serial */,
4B96DECB2EBEE7D100505298 /* SID */,
4BB0A6582044FD3000FB3688 /* SN76489 */,
4B47F3B42E7B9A14005D4DEC /* uPD7002 */,
);
@@ -5269,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 = (
@@ -6163,6 +6207,7 @@
4B894521201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
4B47F3B62E7BAB94005D4DEC /* uPD7002.cpp in Sources */,
4B8318B522D3E548006DB630 /* Macintosh.cpp in Sources */,
4B96DECD2EBEE7D100505298 /* SID.cpp in Sources */,
4B8DF4FA254E36AE00F3433C /* Video.cpp in Sources */,
4B7BA03123C2B19C00B98D9E /* Jasmin.cpp in Sources */,
4B7F188F2154825E00388727 /* MasterSystem.cpp in Sources */,
@@ -6176,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 */,
@@ -6318,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 */,
@@ -6411,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 */,
@@ -6476,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 */,
@@ -6557,6 +6606,7 @@
4BC080D026A257A200D03FD8 /* StaticAnalyser.cpp in Sources */,
4B4DC8211D2C2425003C5BF8 /* Vic20.cpp in Sources */,
4B71368E1F788112008B8ED9 /* Parser.cpp in Sources */,
4B96DECC2EBEE7D100505298 /* SID.cpp in Sources */,
4B12C0ED1FCFA98D005BFD93 /* Keyboard.cpp in Sources */,
4BA0F68E1EEA0E8400E9489E /* ZX8081.cpp in Sources */,
429B13602B1F7BDA006BB4CB /* StaticAnalyser.cpp in Sources */,
@@ -6683,6 +6733,7 @@
4B06AAF82C6460760034D014 /* IntelligentKeyboard.cpp in Sources */,
4B7752B628217EE70073E2C5 /* DSK.cpp in Sources */,
4B06AAD12C645F130034D014 /* 1770.cpp in Sources */,
4B96DECE2EBEE7D100505298 /* SID.cpp in Sources */,
4B778F2523A5EDF40000D260 /* Encoder.cpp in Sources */,
4B778F4223A5F1A70000D260 /* MemoryFuzzer.cpp in Sources */,
4B778F0123A5EBA00000D260 /* MacintoshIMG.cpp in Sources */,
@@ -6778,6 +6829,7 @@
4B778F4623A5F1D80000D260 /* StaticAnalyser.cpp in Sources */,
4B778F1323A5EC890000D260 /* Z80Base.cpp in Sources */,
4B778F2923A5EF030000D260 /* CommodoreROM.cpp in Sources */,
4B96DED32EC3ECDA00505298 /* SIDTests.mm in Sources */,
4B06AADC2C645F720034D014 /* BD500.cpp in Sources */,
4B778F4823A5F1E70000D260 /* StaticAnalyser.cpp in Sources */,
4B92EACA1B7C112B00246143 /* 6502TimingTests.swift in Sources */,
@@ -6833,8 +6885,10 @@
4B778F4023A5F1910000D260 /* z8530.cpp in Sources */,
4B778EFD23A5EB8E0000D260 /* AppleDSK.cpp in Sources */,
4B7752B728217EF40073E2C5 /* Chipset.cpp in Sources */,
4BCF1AD02ECE759000109999 /* FileBundle.cpp in Sources */,
4B06AAF72C64606E0034D014 /* DiskII.cpp in Sources */,
4B778EFB23A5EB7E0000D260 /* HFE.cpp in Sources */,
4BCF1AD52ECF884100109999 /* HostFSHandler.cpp in Sources */,
4BC751B21D157E61006C31D9 /* 6522Tests.swift in Sources */,
4B0DA67D282DCDF300C12F17 /* Instruction.cpp in Sources */,
4B06AAE12C645F8B0034D014 /* Video.cpp in Sources */,

View File

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

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24128" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24412" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24128"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24412"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>

View File

@@ -76,6 +76,7 @@ SOURCES += \
$$SRC/Components/KonamiSCC/*.cpp \
$$SRC/Components/OPx/*.cpp \
$$SRC/Components/RP5C01/*.cpp \
$$SRC/Components/SID/*.cpp \
$$SRC/Components/SAA5050/*.cpp \
$$SRC/Components/Serial/*.cpp \
$$SRC/Components/SN76489/*.cpp \
@@ -145,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 \
@@ -213,6 +215,7 @@ HEADERS += \
$$SRC/Components/OPx/*.hpp \
$$SRC/Components/OPx/Implementation/*.hpp \
$$SRC/Components/RP5C01/*.hpp \
$$SRC/Components/SID/*.hpp \
$$SRC/Components/SAA5050/*.hpp \
$$SRC/Components/Serial/*.hpp \
$$SRC/Components/SN76489/*.hpp \
@@ -301,6 +304,7 @@ HEADERS += \
$$SRC/Storage/Disk/Encodings/MFM/*.hpp \
$$SRC/Storage/Disk/Parsers/*.hpp \
$$SRC/Storage/Disk/Track/*.hpp \
$$SRC/Storage/FileBundle/*.hpp \
$$SRC/Storage/MassStorage/*.hpp \
$$SRC/Storage/MassStorage/Encodings/*.hpp \
$$SRC/Storage/MassStorage/Formats/*.hpp \

View File

@@ -61,6 +61,7 @@ SOURCES += glob.glob('../../Components/KonamiSCC/*.cpp')
SOURCES += glob.glob('../../Components/OPx/*.cpp')
SOURCES += glob.glob('../../Components/RP5C01/*.cpp')
SOURCES += glob.glob('../../Components/SAA5050/*.cpp')
SOURCES += glob.glob('../../Components/SID/*.cpp')
SOURCES += glob.glob('../../Components/SN76489/*.cpp')
SOURCES += glob.glob('../../Components/Serial/*.cpp')
SOURCES += glob.glob('../../Components/uPD7002/*.cpp')
@@ -133,6 +134,7 @@ SOURCES += glob.glob('../../Storage/Disk/Encodings/MFM/*.cpp')
SOURCES += glob.glob('../../Storage/Disk/Parsers/*.cpp')
SOURCES += glob.glob('../../Storage/Disk/Track/*.cpp')
SOURCES += glob.glob('../../Storage/Disk/Data/*.cpp')
SOURCES += glob.glob('../../Storage/FileBundle/*.cpp')
SOURCES += glob.glob('../../Storage/MassStorage/*.cpp')
SOURCES += glob.glob('../../Storage/MassStorage/Encodings/*.cpp')
SOURCES += glob.glob('../../Storage/MassStorage/Formats/*.cpp')

View File

@@ -66,6 +66,7 @@ enum class Line {
PowerOn,
Overflow,
NMI,
Ready,
};
// MARK: - Address bus.
@@ -185,14 +186,24 @@ public:
(inputs_.interrupt_requests & Inputs::InterruptRequest::PowerOn);
break;
// Level triggered.
// Level triggered interrupts.
case Line::Reset: level_sample(Inputs::InterruptRequest::Reset); break;
case Line::IRQ: level_sample(Inputs::InterruptRequest::IRQ); break;
// Edge triggered.
case Line::Overflow: edge_sample(Inputs::InterruptRequest::Reset, inputs_.overflow); break;
// Edge triggered interrupts.
case Line::NMI: edge_sample(Inputs::InterruptRequest::NMI, inputs_.nmi); break;
// Leval-capturing state.
case Line::Ready: inputs_.ready = value; break;
// Edge-triggered state.
case Line::Overflow:
if(!inputs_.overflow && value) {
registers_.flags.set_per<Flag::Overflow>(Flag::Overflow);
}
inputs_.overflow = value;
break;
default:
__builtin_unreachable();
}

View File

@@ -0,0 +1,214 @@
//
// BiquadFilter.hpp
// Clock Signal
//
// Created by Thomas Harte on 12/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include <cassert>
#include <cmath>
#include <numbers>
namespace SignalProcessing {
/*!
A biquad[ratic] filter approximates the real analogue thing in taking a 1d PCM signal and applying a
filter to it as a function of the current input plus the two most-recent inputs plus the two most-recent outputs.
So both IIR and three-tap FIR filters are degenerate cases of the biquad.
It is used quite often in real designs, hence an implementation of this filter specifically.
... and the below is largely textbook; I can't claim any great knowledge. I am especially indebted to
the W3C Group's audio EQ cookbook at https://www.w3.org/TR/audio-eq-cookbook/ .
*/
class BiquadFilter {
public:
enum class Type {
LowPass,
HighPass,
BandPass,
Notch,
AllPass,
Peaking,
LowShelf,
HighShelf
};
// Default construction: a filter that produces _nothing_.
BiquadFilter() {}
BiquadFilter(
const Type type,
const float sample_rate,
const float frequency,
const float resonance = 0.707f,
const float gain = 8,
const bool normalise = true
) {
configure(type, sample_rate, frequency, resonance, gain, normalise);
}
void configure(
const Type type,
const float sample_rate,
const float frequency,
const float resonance = 0.707f,
const float gain = 8,
const bool normalise = true
) {
const float w0 = 2.0f * std::numbers::pi_v<float> * frequency / sample_rate;
const float alpha = std::sin(w0) / (2.0f * resonance);
const float cos_w0 = std::cos(w0);
float coefficients[5];
float magnitude = 1.0f;
switch(type) {
case Type::LowPass:
coefficients[0] = (1.0f - cos_w0) / 2.0f;
coefficients[1] = 1.0f - cos_w0;
coefficients[2] = (1.0f - cos_w0) / 2.0f;
magnitude = 1.0f + alpha;
coefficients[3] = -2.0f * cos_w0;
coefficients[4] = 1.0f - alpha;
break;
case Type::HighPass:
coefficients[0] = (1.0f - cos_w0) / 2.0f;
coefficients[1] = -(1.0f + cos_w0);
coefficients[2] = (1.0f - cos_w0) / 2.0f;
magnitude = 1.0f + alpha;
coefficients[3] = -2.0f * cos_w0;
coefficients[4] = 1.0f - alpha;
break;
case Type::BandPass:
coefficients[0] = alpha;
coefficients[1] = 0.0f;
coefficients[2] = -alpha;
magnitude = 1.0f + alpha;
coefficients[3] = -2.0f * cos_w0;
coefficients[0] = 1.0f - alpha;
break;
case Type::Notch:
coefficients[0] = 1.0f;
coefficients[1] = -2.0f * cos_w0;
coefficients[2] = 1.0f;
magnitude = 1.0f + alpha;
coefficients[3] = -2.0f * cos_w0;
coefficients[4] = 1.0f - alpha;
break;
case Type::AllPass:
coefficients[0] = 1.0f - alpha;
coefficients[1] = -2.0f * cos_w0;
coefficients[2] = 1.0f + alpha;
magnitude = 1.0f + alpha;
coefficients[3] = -2.0f * cos_w0;
coefficients[4] = 1.0f - alpha;
break;
case Type::Peaking: {
const float a = std::pow(10.0f, gain / 40.0f);
coefficients[0] = 1.0f + (alpha * a);
coefficients[1] = -2.0f * cos_w0;
coefficients[2] = 1.0f - (alpha * a);
magnitude = 1.0f + (alpha / a);
coefficients[3] = -2.0f * cos_w0;
coefficients[4] = 1.0f - (alpha / a);
} break;
case Type::LowShelf: {
const float a_ls = std::pow(10.0f, gain / 40.0f);
const float sqrt_a = std::sqrt(a_ls);
const float alpha_ls =
std::sin(w0) / 2.0f * std::sqrt((a_ls + 1.0f / a_ls) * (1.0f / resonance - 1.0f) + 2.0f);
coefficients[0] = a_ls * ((a_ls + 1.0f) - (a_ls - 1.0f) * cos_w0 + 2.0f * sqrt_a * alpha_ls);
coefficients[1] = 2.0f * a_ls * ((a_ls - 1.0f) - (a_ls + 1.0f) * cos_w0);
coefficients[2] = a_ls * ((a_ls + 1.0f) - (a_ls - 1.0f) * cos_w0 - 2.0f * sqrt_a * alpha_ls);
magnitude = (a_ls + 1.0f) + (a_ls - 1.0f) * cos_w0 + 2.0f * sqrt_a * alpha_ls;
coefficients[3] = -2.0f * ((a_ls - 1) + (a_ls + 1) * cos_w0);
coefficients[4] = (a_ls + 1.0f) + (a_ls - 1.0f) * cos_w0 - 2.0f * sqrt_a * alpha_ls;
} break;
case Type::HighShelf: {
const float a_hs = std::pow(10.0f, gain / 40.0f);
const float sqrt_a_hs = std::sqrt(a_hs);
const float alpha_hs =
std::sin(w0) / 2.0f * std::sqrt((a_hs + 1.0f / a_hs) * (1.0f / resonance - 1.0f) + 2.0f);
coefficients[0] = a_hs * ((a_hs + 1.0f) + (a_hs - 1.0f) * cos_w0 + 2.0f * sqrt_a_hs * alpha_hs);
coefficients[1] = -2.0f * a_hs * ((a_hs - 1.0f) + (a_hs + 1.0f) * cos_w0);
coefficients[2] = a_hs * ((a_hs + 1.0f) + (a_hs - 1.0f) * cos_w0 - 2.0f * sqrt_a_hs * alpha_hs);
magnitude = (a_hs + 1.0f) - (a_hs - 1.0f) * cos_w0 + 2.0f * sqrt_a_hs * alpha_hs;
coefficients[3] = 2.0f * ((a_hs - 1.0f) - (a_hs + 1.0f) * cos_w0);
coefficients[4] = (a_hs + 1.0f) - (a_hs - 1.0f) * cos_w0 - 2.0f * sqrt_a_hs * alpha_hs;
} break;
}
if(normalise) {
for(int c = 0; c < 5; c++) {
coefficients[c] /= magnitude;
}
}
for(int c = 0; c < 5; c++) {
#ifdef FIXED
coefficients_[c] = FixedType(coefficients[c] * FixedMultiplier);
#else
coefficients_[c] = coefficients[c];
#endif
}
}
int16_t apply(const int16_t input) {
#ifdef FIXED
const auto applied =
coefficients_[0] * input +
coefficients_[1] * inputs_[0] +
coefficients_[2] * inputs_[1] -
coefficients_[3] * outputs_[0] -
coefficients_[4] * outputs_[1];
const auto output = int16_t(applied >> FixedShift);
#else
const float output =
coefficients_[0] * float(input) +
coefficients_[1] * inputs_[0] +
coefficients_[2] * inputs_[1] -
coefficients_[3] * outputs_[0] -
coefficients_[4] * outputs_[1];
#endif
inputs_[1] = inputs_[0];
inputs_[0] = input;
outputs_[1] = outputs_[0];
outputs_[0] = output;
return int16_t(output);
}
private:
#ifdef FIXED
int16_t inputs_[2]{};
int16_t outputs_[2]{};
using FixedType = int64_t;
static constexpr int FixedShift = 48;
static constexpr auto FixedMultiplier = static_cast<float>(int64_t(1) << FixedShift);
FixedType coefficients_[5]{};
#else
float inputs_[2]{};
float outputs_[2]{};
float coefficients_[5]{};
#endif
// Coefficients indices versus common textbook terms:
// 0 = b0; 1 = b1; 2 = b2; 3 = a1; 4 = a2
};
}

View File

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

View File

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

View File

@@ -75,7 +75,7 @@ Track *DiskImageHolder<T>::track_at_position(Track::Address address) const {
template <typename T>
DiskImageHolder<T>::~DiskImageHolder() {
if(update_queue_) update_queue_->flush();
if(update_queue_) update_queue_->lock_flush();
}
template <typename T>

View File

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

View File

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

View File

@@ -16,13 +16,19 @@
using namespace Storage::Disk;
Drive::Drive(int input_clock_rate, int revolutions_per_minute, int number_of_heads, ReadyType rdy_type):
Drive::Drive(
const int input_clock_rate,
const int revolutions_per_minute,
const int number_of_heads,
const ReadyType rdy_type
) :
Storage::TimedEventLoop(input_clock_rate),
available_heads_(number_of_heads),
ready_type_(rdy_type) {
set_rotation_speed(revolutions_per_minute);
const auto seed = std::default_random_engine::result_type(std::chrono::system_clock::now().time_since_epoch().count());
const auto seed =
std::default_random_engine::result_type(std::chrono::system_clock::now().time_since_epoch().count());
std::default_random_engine randomiser(seed);
// Get at least 64 bits of random information; rounding is likey to give this a slight bias.
@@ -34,16 +40,21 @@ Drive::Drive(int input_clock_rate, int revolutions_per_minute, int number_of_hea
}
}
Drive::Drive(int input_clock_rate, int number_of_heads, ReadyType rdy_type) : Drive(input_clock_rate, 300, number_of_heads, rdy_type) {}
Drive::Drive(
const int input_clock_rate,
const int number_of_heads,
const ReadyType rdy_type
) : Drive(input_clock_rate, 300, number_of_heads, rdy_type) {}
void Drive::set_rotation_speed(float revolutions_per_minute) {
void Drive::set_rotation_speed(const float revolutions_per_minute) {
// Rationalise the supplied speed so that cycles_per_revolution_ is exact.
cycles_per_revolution_ = int(0.5f + float(get_input_clock_rate()) * 60.0f / revolutions_per_minute);
// From there derive the appropriate rotational multiplier and possibly update the
// count of cycles since the index hole proportionally.
const float new_rotational_multiplier = float(cycles_per_revolution_) / float(get_input_clock_rate());
cycles_since_index_hole_ = Cycles::IntType(float(cycles_since_index_hole_) * new_rotational_multiplier / rotational_multiplier_);
cycles_since_index_hole_ =
Cycles::IntType(float(cycles_since_index_hole_) * new_rotational_multiplier / rotational_multiplier_);
rotational_multiplier_ = new_rotational_multiplier;
cycles_since_index_hole_ %= cycles_per_revolution_;
}
@@ -75,14 +86,16 @@ bool Drive::has_disk() const {
}
ClockingHint::Preference Drive::preferred_clocking() const {
return (!has_disk_ || (time_until_motor_transition == Cycles(0) && !disk_is_rotating_)) ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime;
return (
!has_disk_ || (time_until_motor_transition == Cycles(0) && !disk_is_rotating_)
) ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime;
}
bool Drive::get_is_track_zero() const {
return head_position_ == HeadPosition(0);
}
void Drive::step(HeadPosition offset) {
void Drive::step(const HeadPosition offset) {
if(offset == HeadPosition(0)) {
return;
}
@@ -109,7 +122,7 @@ void Drive::step(HeadPosition offset) {
did_step(head_position_);
}
Track *Drive::step_to(HeadPosition offset) {
Track *Drive::step_to(const HeadPosition offset) {
HeadPosition old_head_position = head_position_;
head_position_ = std::max(offset, HeadPosition(0));
@@ -160,7 +173,7 @@ bool Drive::get_is_ready() const {
return is_ready_;
}
void Drive::set_motor_on(bool motor_is_on) {
void Drive::set_motor_on(const bool motor_is_on) {
// Do nothing if the input hasn't changed.
if(motor_input_is_on_ == motor_is_on) return;
motor_input_is_on_ = motor_is_on;
@@ -195,7 +208,7 @@ bool Drive::get_index_pulse() const {
return index_pulse_remaining_ > Cycles(0);
}
void Drive::set_event_delegate(Storage::Disk::Drive::EventDelegate *delegate) {
void Drive::set_event_delegate(Storage::Disk::Drive::EventDelegate *const delegate) {
event_delegate_ = delegate;
}
@@ -237,13 +250,21 @@ void Drive::run_for(const Cycles cycles) {
if(cycles_until_bits_written_ > zero) {
Storage::Time cycles_to_run_for_time(static_cast<int>(cycles_to_run_for));
if(cycles_until_bits_written_ <= cycles_to_run_for_time) {
if(event_delegate_) event_delegate_->process_write_completed();
if(cycles_until_bits_written_ <= cycles_to_run_for_time)
cycles_until_bits_written_.set_zero();
else
cycles_until_bits_written_ -= cycles_to_run_for_time;
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) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
//
// 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_;
}
Storage::FileHolder LocalFSFileBundle::open(const std::string &name, const Storage::FileMode mode) {
return Storage::FileHolder(base_path_ + name, mode);
}
bool LocalFSFileBundle::erase(const std::string &name) {
return !remove((base_path_ + name).c_str());
}

View File

@@ -0,0 +1,44 @@
//
// FileBundle.hpp
// Clock Signal
//
// Created by Thomas Harte on 19/11/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Storage/FileHolder.hpp"
#include <optional>
#include <string>
namespace Storage::FileBundle {
/*!
A File Bundle is a collection of individual files, abstracted from whatever media they might
be one.
Initial motivation is allowing some machines direct local filesystem access. An attempt has
been made to draft this in such a way as to allow it to do things like expose ZIP files as
bundles in the future.
*/
struct FileBundle {
virtual std::optional<std::string> key_file() = 0;
virtual FileHolder open(const std::string &, FileMode) = 0;
virtual bool erase(const std::string &) = 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;
private:
std::string key_file_;
std::string base_path_;
};
};

View File

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

View File

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

View File

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

View File

@@ -59,6 +59,7 @@ set(CLK_SOURCES
Components/OPx/OPLL.cpp
Components/RP5C01/RP5C01.cpp
Components/SAA5050/SAA5050.cpp
Components/SID/SID.cpp
Components/SN76489/SN76489.cpp
Components/Serial/Line.cpp
Components/uPD7002/uPD7002.cpp
@@ -129,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
@@ -223,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