// // Enterprise.cpp // Clock Signal // // Created by Thomas Harte on 10/06/2021. // Copyright © 2021 Thomas Harte. All rights reserved. // #include "Enterprise.hpp" #include "Dave.hpp" #include "EXDos.hpp" #include "HostFSHandler.hpp" #include "Keyboard.hpp" #include "Nick.hpp" #include "Machines/MachineTypes.hpp" #include "Machines/Utility/Typer.hpp" #include "Activity/Source.hpp" #include "Analyser/Static/Enterprise/Target.hpp" #include "ClockReceiver/JustInTime.hpp" #include "Outputs/Log.hpp" #include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "Processors/Z80/Z80.hpp" #include #include namespace { using Logger = Log::Logger; static constexpr size_t ram_size(const Analyser::Static::Enterprise::Target::Model model) { switch(model) { case Analyser::Static::Enterprise::Target::Model::Enterprise64: return 64 * 1024; default: case Analyser::Static::Enterprise::Target::Model::Enterprise128: return 128 * 1024; case Analyser::Static::Enterprise::Target::Model::Enterprise256: return 256 * 1024; } } } namespace Enterprise { /* Notes to self on timing: Nick divides each line into 57 windows; each window lasts 16 cycles and dedicates the first 10 of those to VRAM accesses, leaving the final six for a Z80 video RAM access if one has been requested. The Z80 has a separate, asynchronous 4Mhz clock. That's that. The documentation is also very forward in emphasising that Nick generates phaselocked (i.e. in-phase) PAL video. So: 57*16 = 912 cycles/line. A standard PAL line lasts 64µs and during that time outputs 283.7516 colour cycles. I shall _guess_ that the Enterprise stretches each line to 284 colour cycles rather than reducing it to 283. Therefore 912 cycles occurs in 284/283.7516 * 64 µs. So one line = 181760000 / 2837516 µs = 45440000 / 709379 µs => one cycle = 45440000 / 709379*912 = 45440000 / 646953648 = 2840000 / 40434603 µs => clock rate of 40434603 / 2840000 Mhz And, therefore, the ratio to a 4Mhz Z80 clock is: 40434603 / (2840000 * 4) = 40434603 / 11360000 i.e. roughly 3.55 Nick cycles per Z80 cycle. If that's true then the 6-cycle window is around 1.69 Z80 cycles long. Given that the Z80 clock in an Enterprise can be stopped in half-cycle increments only, the Z80 can only be guaranteed to have around a 1.19 cycle minimum for its actual access. I'm therefore further postulating that the clock stoppage takes place so as to align the final cycle of a relevant access over the available window. */ template < Analyser::Static::Enterprise::Target::Model model, 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, public MachineTypes::MediaTarget, public MachineTypes::ScanProducer, public MachineTypes::SoftResettable, public MachineTypes::TimedMachine, public Utility::TypeRecipient { private: static constexpr double clock_rate = is_6mhz ? 6'000'000.0 : 4'000'000.0; using NickType = std::conditional_t, JustInTimeActor>; public: ConcreteMachine(const Analyser::Static::Enterprise::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : z80_(*this), nick_(ram_.end() - 65536), dave_audio_(audio_queue_), 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); speaker_.set_input_rate(float(clock_rate) / float(dave_divider)); ROM::Request request; using Target = Analyser::Static::Enterprise::Target; // Pick one or more EXOS ROMs. switch(target.exos_version) { case Target::EXOSVersion::v10: request = request && ROM::Request(ROM::Name::EnterpriseEXOS10); break; case Target::EXOSVersion::v20: request = request && ROM::Request(ROM::Name::EnterpriseEXOS20); break; case Target::EXOSVersion::v21: request = request && ROM::Request(ROM::Name::EnterpriseEXOS21); break; case Target::EXOSVersion::v23: request = request && ROM::Request(ROM::Name::EnterpriseEXOS23); break; case Target::EXOSVersion::Any: request = request && ( ROM::Request(ROM::Name::EnterpriseEXOS10) || ROM::Request(ROM::Name::EnterpriseEXOS20) || ROM::Request(ROM::Name::EnterpriseEXOS21) || ROM::Request(ROM::Name::EnterpriseEXOS23) ); break; default: break; } // Similarly pick one or more BASIC ROMs. switch(target.basic_version) { case Target::BASICVersion::v10: request = request && ( ROM::Request(ROM::Name::EnterpriseBASIC10) || ( ROM::Request(ROM::Name::EnterpriseBASIC10Part1) && ROM::Request(ROM::Name::EnterpriseBASIC10Part2) ) ); break; case Target::BASICVersion::v11: request = request && ( ROM::Request(ROM::Name::EnterpriseBASIC11) || ROM::Request(ROM::Name::EnterpriseBASIC11Suffixed) ); break; case Target::BASICVersion::v21: request = request && ROM::Request(ROM::Name::EnterpriseBASIC21); break; case Target::BASICVersion::Any: request = request && ( ROM::Request(ROM::Name::EnterpriseBASIC10) || ( ROM::Request(ROM::Name::EnterpriseBASIC10Part1) && ROM::Request(ROM::Name::EnterpriseBASIC10Part2) ) || ROM::Request(ROM::Name::EnterpriseBASIC11) || ROM::Request(ROM::Name::EnterpriseBASIC21) ); break; default: break; } // Possibly add in a DOS. switch(target.dos) { case Target::DOS::EXDOS: request = request && ROM::Request(ROM::Name::EnterpriseEXDOS); break; default: break; } // Get and validate ROMs. auto roms = rom_fetcher(request); if(!request.validate(roms)) { throw ROMMachine::Error::MissingROMs; } const auto install = [&](const ROM::Name rom_name, auto &destination) { const auto source = roms.find(rom_name); if(source == roms.end()) { return false; } std::copy_n( source->second.begin(), std::min(destination.size(), source->second.size()), destination.begin() ); return true; }; // Extract the appropriate EXOS ROM. exos_.fill(0xff); for(const auto rom_name: { ROM::Name::EnterpriseEXOS10, ROM::Name::EnterpriseEXOS20, ROM::Name::EnterpriseEXOS21, ROM::Name::EnterpriseEXOS23 }) { if(install(rom_name, exos_)) { break; } } // Extract the appropriate BASIC ROM[s] (if any). basic_.fill(0xff); bool has_basic = false; for(const auto rom_name: { ROM::Name::EnterpriseBASIC10, ROM::Name::EnterpriseBASIC11, ROM::Name::EnterpriseBASIC11Suffixed, ROM::Name::EnterpriseBASIC21 }) { if(install(rom_name, basic_)) { has_basic = true; break; } } if(!has_basic) { const auto basic1 = roms.find(ROM::Name::EnterpriseBASIC10Part1); const auto basic2 = roms.find(ROM::Name::EnterpriseBASIC10Part2); if(basic1 != roms.end() && basic2 != roms.end()) { std::copy_n( basic1->second.begin(), std::min(size_t(8192), basic1->second.size()), &basic_[0x0000] ); std::copy_n( basic2->second.begin(), std::min(size_t(8192), basic2->second.size()), &basic_[0x2000] ); } } // Extract the appropriate DOS ROMs. epdos_rom_.fill(0xff); install(ROM::Name::EnterpriseEPDOS, epdos_rom_); exdos_rom_.fill(0xff); install(ROM::Name::EnterpriseEXDOS, exdos_rom_); // 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(); // Take a reasonable guess at the initial memory configuration: // put EXOS into the first bank since this is a Z80 and therefore // starts from address 0; the third instruction in EXOS is a jump // to $c02e so it's reasonable to assume EXOS is in the highest bank // too, and it appears to act correctly if it's the first 16kb that's // in the highest bank. From there I guess: all banks are initialised // to 0. page<0>(0x00); page<1>(0x00); page<2>(0x00); page<3>(0x00); // Pass on any media. insert_media(target.media); type_string(target.loading_command); // Ensure the splash screen is automatically skipped if any media has been provided. if(!target.media.empty()) { should_skip_splash_screen_ = !target.media.empty(); typer_delay_ = 2; } } ~ConcreteMachine() { audio_queue_.lock_flush(); } // MARK: - Z80::BusHandler. forceinline void advance_nick(const HalfCycles duration) { if(nick_ += duration) { const auto nick = nick_.get(); const bool nick_interrupt_line = nick->get_interrupt_line(); if(nick_interrupt_line && !previous_nick_interrupt_line_) { set_interrupts(uint8_t(Dave::Interrupt::Nick), nick_.last_sequence_point_overrun()); } previous_nick_interrupt_line_ = nick_interrupt_line; } } forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) { using PartialMachineCycle = CPU::Z80::PartialMachineCycle; const uint16_t address = cycle.address ? *cycle.address : 0x0000; // Calculate an access penalty, if applicable. // // Rule applied here, which is slightly inferred: // // Non-video reads and writes are delayed by exactly a cycle or not delayed at all, // depending on the programmer's configuration of Dave. // // Video reads and writes, and Nick port accesses, are delayed so that the last // clock cycle of the machine cycle falls wholly inside the designated Z80 access // window, per Nick. // // The switch statement below just attempts to implement that logic. // HalfCycles penalty; switch(cycle.operation) { default: break; // For non-video pauses, insert during the initial part of the bus cycle. case PartialMachineCycle::ReadStart: case PartialMachineCycle::WriteStart: if(!is_video_[address >> 14] && wait_mode_ == WaitMode::OnAllAccesses) { penalty = dave_delay_; } break; case PartialMachineCycle::ReadOpcodeStart: { if(is_video_[address >> 14]) { // Query Nick for the amount of delay that would occur with one cycle left // in this read opcode. const auto delay_time = nick_.time_since_flush(HalfCycles(2)); const auto delay = nick_.get()->get_time_until_z80_slot(delay_time); penalty = nick_.back_map(delay, delay_time); } else if(wait_mode_ != WaitMode::None) { penalty = dave_delay_; } } break; // Video pauses: insert right at the end of the bus cycle. case PartialMachineCycle::Write: // Ensure all video that should have been collected prior to // this write has been. if(is_video_[address >> 14]) { nick_.flush(); } [[fallthrough]]; case PartialMachineCycle::Read: if(is_video_[address >> 14]) { // Get delay, in Nick cycles, for a Z80 access that occurs in 0.5 // cycles from now (i.e. with one cycle left to run). const auto delay_time = nick_.time_since_flush(HalfCycles(1)); const auto delay = nick_.get()->get_time_until_z80_slot(delay_time); penalty = nick_.back_map(delay, delay_time); } break; case PartialMachineCycle::Input: case PartialMachineCycle::Output: { if((address & 0xf0) == 0x80) { // Get delay, in Nick cycles, for a Z80 access that occurs in 0.5 // cycles from now (i.e. with one cycle left to run). const auto delay_time = nick_.time_since_flush(HalfCycles(1)); const auto delay = nick_.get()->get_time_until_z80_slot(delay_time); penalty = nick_.back_map(delay, delay_time); } } } const HalfCycles full_length = cycle.length + penalty; time_since_audio_update_ += full_length; advance_nick(full_length); if(dave_timer_ += full_length) { set_interrupts(dave_timer_.get()->get_new_interrupts(), dave_timer_.last_sequence_point_overrun()); } // The WD/etc runs at a nominal 8Mhz. if constexpr (has_disk_controller) { exdos_.run_for(Cycles(full_length.get())); } switch(cycle.operation) { default: break; case PartialMachineCycle::Interrupt: *cycle.value = 0xff; break; case PartialMachineCycle::Input: switch(address & 0xff) { default: Logger::error().append("Unhandled input from %02x", address & 0xff); *cycle.value = 0xff; break; case 0x10: case 0x11: case 0x12: case 0x13: case 0x14: case 0x15: case 0x16: case 0x17: if constexpr (has_disk_controller) { *cycle.value = exdos_.read(address); } else { *cycle.value = 0xff; } break; case 0x18: case 0x19: case 0x1a: case 0x1b: case 0x1c: case 0x1d: case 0x1e: case 0x1f: if constexpr (has_disk_controller) { *cycle.value = exdos_.get_control_register(); } else { *cycle.value = 0xff; } break; case 0x80: case 0x81: case 0x82: case 0x83: case 0x84: case 0x85: case 0x86: case 0x87: case 0x88: case 0x89: case 0x8a: case 0x8b: case 0x8c: case 0x8d: case 0x8e: case 0x8f: *cycle.value = nick_->read(); break; case 0xb0: *cycle.value = pages_[0]; break; case 0xb1: *cycle.value = pages_[1]; break; case 0xb2: *cycle.value = pages_[2]; break; case 0xb3: *cycle.value = pages_[3]; break; case 0xb4: *cycle.value = (nick_->get_interrupt_line() ? 0x10 : 0x00) | dave_timer_->get_divider_state() | interrupt_state_; break; case 0xb5: if(active_key_line_ < key_lines_.size()) { *cycle.value = key_lines_[active_key_line_]; } else { *cycle.value = 0xff; } break; case 0xb6: { // TODO: selected keyboard row, 0 to 9, should return one bit of joystick // input. That being the case: // // b0: joystick input // b1, b2: unused (in theory read from control port, but not used by any hardware) // b3: 0 = printer ready; 1 = not ready // b4: serial, data in // b5: serial, status in // b6: tape input volume level, 0 = high, 1 = low // b7: tape data input *cycle.value = 0xff; } break; } break; case PartialMachineCycle::Output: switch(address & 0xff) { default: Logger::error().append("Unhandled output: %02x to %02x", *cycle.value, address & 0xff); break; case 0x10: case 0x11: case 0x12: case 0x13: case 0x14: case 0x15: case 0x16: case 0x17: if constexpr(has_disk_controller) { exdos_.write(address, *cycle.value); } break; case 0x18: case 0x19: case 0x1a: case 0x1b: case 0x1c: case 0x1d: case 0x1e: case 0x1f: if constexpr(has_disk_controller) { exdos_.set_control_register(*cycle.value); } break; case 0x80: case 0x81: case 0x82: case 0x83: case 0x84: case 0x85: case 0x86: case 0x87: case 0x88: case 0x89: case 0x8a: case 0x8b: case 0x8c: case 0x8d: case 0x8e: case 0x8f: nick_->write(address, *cycle.value); break; case 0xb0: page<0>(*cycle.value); break; case 0xb1: page<1>(*cycle.value); break; case 0xb2: page<2>(*cycle.value); break; case 0xb3: page<3>(*cycle.value); break; case 0xbf: switch((*cycle.value >> 2)&3) { default: wait_mode_ = WaitMode::None; break; case 0: wait_mode_ = WaitMode::OnAllAccesses; break; case 1: wait_mode_ = WaitMode::OnM1; break; } // Dave delays (i.e. those affecting memory areas not associated with Nick) // are one cycle in 8Mhz mode, two cycles in 12Mhz mode. dave_delay_ = HalfCycles(2 + ((*cycle.value)&2)); [[fallthrough]]; case 0xa0: case 0xa1: case 0xa2: case 0xa3: case 0xa4: case 0xa5: case 0xa6: case 0xa7: case 0xa8: case 0xa9: case 0xaa: case 0xab: case 0xac: case 0xad: case 0xae: case 0xaf: update_audio(); dave_audio_.write(address, *cycle.value); dave_timer_->write(address, *cycle.value); break; case 0xb4: interrupt_mask_ = *cycle.value & 0x55; interrupt_state_ &= ~*cycle.value; update_interrupts(); break; case 0xb5: // Logic here: the ROM scans the keyboard by checking ascending // lines. It also seems to provide a line of 0 when using port B5 // for non-keyboard uses. // // So: use the rollover from line 9 back to line 0 as a trigger to // spot that a scan of the keyboard just finished. Which makes it // time to enqueue the next keypress. // // Re: should_skip_splash_screen_ and typer_delay_, assume that a // single keypress is necessary to get past the Enterprise splash // screen, then a pause in keypressing while BASIC or whatever // starts up, then presses can resume. if(active_key_line_ == 9 && !(*cycle.value & 0xf) && (should_skip_splash_screen_ || typer_)) { if(should_skip_splash_screen_) { set_key_state(uint16_t(Key::Space), typer_delay_); if(typer_delay_) { --typer_delay_; } else { typer_delay_ = 60; should_skip_splash_screen_ = false; } } else { if(!typer_delay_) { if(!typer_->type_next_character()) { clear_all_keys(); typer_ = nullptr; } } else { --typer_delay_; } } } active_key_line_ = *cycle.value & 0xf; // TODO: // // b4: strobe output for printer // b5: tape sound control (?) // b6: tape motor control 1, 1 = on // b7: tape motor control 2, 1 = on break; case 0xb6: // Just 8 bits of printer data. Logger::info().append("TODO: printer output: %02x", *cycle.value); break; case 0xb7: // b0 = serial data out // b1 = serial status out Logger::info().append("TODO: serial output: %02x", *cycle.value); break; } break; case PartialMachineCycle::ReadOpcode: // 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 { *cycle.value = 0xff; } break; case PartialMachineCycle::Write: if(write_pointers_[address >> 14]) { write_pointers_[address >> 14][address] = *cycle.value; } break; } return penalty; } void flush_output(const int outputs) final { if(outputs & Output::Video) { nick_.flush(); } if(outputs & Output::Audio) { update_audio(); audio_queue_.perform(); } } private: // MARK: - Memory layout std::array ram_{}; std::array exos_; std::array basic_; std::array exdos_rom_; std::array epdos_rom_; std::array host_fs_rom_; static constexpr auto MinRAMSlot = uint8_t(0x100 - (ram_size(model) >> 14)); /// @returns A pointer to the start of the RAM segment representing @c page if any; otherwise @c nullptr. uint8_t *ram_segment(const uint8_t page) { if(page < MinRAMSlot) return nullptr; return &ram_[size_t((page - MinRAMSlot) << 14)]; } struct ROMPage { const uint8_t *rom; uint8_t page_offset; operator bool() const { return bool(rom); } uint8_t operator[](const size_t offset) const { return rom[page_offset * 0x4000 + offset]; } }; /// @returns A pointer to the ROM segment representing @c page if any; otherwise an object that converts to bool @c false. The returned object /// names both the start of the ROM and how many pages into it this page rests. ROMPage rom_segment(const uint8_t page) { const auto rom_segment = [&](const uint8_t base, auto &source) -> ROMPage { if(page < base || page >= base + source.size() / 0x4000) { return { nullptr, 0 }; } return { source.data(), uint8_t(page - base) }; }; // This is where I've effectively dictated the overall 22-bit RAM layout. // The first argument before each ROM dictates its starting page. if(const auto segment = rom_segment(0, exos_); segment.rom) return segment; if(const auto segment = rom_segment(16, basic_); segment.rom) return segment; if(const auto segment = rom_segment(32, exdos_rom_); segment.rom) return segment; if(const auto segment = rom_segment(48, epdos_rom_); segment.rom) return segment; if(const auto segment = rom_segment(64, host_fs_rom_); segment.rom) return segment; return { nullptr, 0 }; } // Ephemeral, user-set state, representing the current memory map as viewed from the Z80. const uint8_t *read_pointers_[4] = {nullptr, nullptr, nullptr, nullptr}; uint8_t *write_pointers_[4] = {nullptr, nullptr, nullptr, nullptr}; uint8_t pages_[4] = {0x80, 0x80, 0x80, 0x80}; /// Pages whatever is supposed to be at @c offset into memory at @c slot, whether ROM or RAM, and updates the /// @c test_host_fs_traps_ and relevant @c is_video_ flags. template void page(const uint8_t offset) { const auto apply = [&](const uint8_t *const read, uint8_t *const write) { read_pointers_[slot] = read ? read - (slot * 0x4000) : nullptr; write_pointers_[slot] = write ? write - (slot * 0x4000) : nullptr; }; pages_[slot] = offset; const auto rom = rom_segment(offset); if constexpr (slot == 3) { if(rom) { test_host_fs_traps_ = rom.rom == host_fs_rom_.data(); } else { test_host_fs_traps_ = false; } } if(rom) { apply(rom.rom + rom.page_offset * 0x4000, nullptr); is_video_[slot] = false; return; } auto pointer = ram_segment(offset); if(pointer) { is_video_[slot] = offset >= 0xfc; // TODO: this hard-codes a 64kb video assumption. apply(pointer, pointer); return; } apply(nullptr, nullptr); } // MARK: - Memory Timing // The wait mode affects all memory accesses _outside of the video area_. enum class WaitMode { None, OnM1, OnAllAccesses } wait_mode_ = WaitMode::OnAllAccesses; bool is_video_[4]{}; // MARK: - ScanProducer void set_scan_target(Outputs::Display::ScanTarget *const scan_target) override { nick_.get()->set_scan_target(scan_target); } Outputs::Display::ScanStatus get_scaled_scan_status() const override { return nick_.get()->get_scaled_scan_status(); } void set_display_type(const Outputs::Display::DisplayType display_type) final { nick_.get()->set_display_type(display_type); } Outputs::Display::DisplayType get_display_type() const final { return nick_.get()->get_display_type(); } // MARK: - AudioProducer Outputs::Speaker::Speaker *get_speaker() final { return &speaker_; } // MARK: - TimedMachine void run_for(const Cycles cycles) override { z80_.run_for(cycles); } // MARK: - Resettable. void soft_reset() override { page<0>(0x00); page<1>(0x00); page<2>(0x00); page<3>(0x00); z80_.set_power_on_reset(); } // MARK: - KeyboardMachine Enterprise::KeyboardMapper keyboard_mapper_; KeyboardMapper *keyboard_mapper() final { return &keyboard_mapper_; } uint8_t active_key_line_ = 0; std::array key_lines_; void set_key_state(const uint16_t key, const bool is_pressed) final { if(is_pressed) { key_lines_[key >> 8] &= ~uint8_t(key); } else { key_lines_[key >> 8] |= uint8_t(key); } } void clear_all_keys() final { key_lines_.fill(0xff); } // MARK: - Utility::TypeRecipient void type_string(const std::wstring &string) final { Utility::TypeRecipient::add_typer(string); if(z80_.get_is_resetting()) { should_skip_splash_screen_ = true; typer_delay_ = 1; } else { should_skip_splash_screen_ = false; typer_delay_ = 0; } } bool can_type(const wchar_t c) const final { return Utility::TypeRecipient::can_type(c); } bool should_skip_splash_screen_ = false; int typer_delay_ = 30; // MARK: - MediaTarget bool insert_media(const Analyser::Static::Media &media) final { if constexpr (has_disk_controller) { if(!media.disks.empty()) { exdos_.set_disk(media.disks.front(), 0); } } 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); update_interrupts(offset); } void update_interrupts(const HalfCycles offset = HalfCycles(0)) { z80_.set_interrupt_line((interrupt_state_ >> 1) & interrupt_mask_, offset); } // MARK: - Chips. CPU::Z80::Processor z80_; NickType nick_; bool previous_nick_interrupt_line_ = false; // Cf. timing guesses above. Concurrency::AsyncTaskQueue audio_queue_; Dave::Audio dave_audio_; Outputs::Speaker::PullLowpass speaker_; HalfCycles time_since_audio_update_; HalfCycles dave_delay_ = HalfCycles(2); // The divider supplied to the JustInTimeActor and the manual divider used in // update_audio() should match. static constexpr int dave_divider = 8; JustInTimeActor dave_timer_; inline void update_audio() { speaker_.run_for( audio_queue_, time_since_audio_update_.divide(dave_divider) ); } // MARK: - EXDos card. EXDos exdos_; // MARK: - Host FS. HostFSHandler host_fs_; std::unordered_set host_fs_traps_; bool test_host_fs_traps_ = false; /// Reads from mamory as currently laid out. uint8_t hostfs_read(const uint16_t address) override { if(read_pointers_[address >> 14]) { return read_pointers_[address >> 14][address]; } else { return 0xff; } } /// @returns The page that should be used to access memory at @c address within the current user memory map. /// This is purely an EXOS construct. It has no basis in hardware. uint8_t user_page(const uint16_t address) { const auto page_id = address >> 14; return read_pointers_[0xbffc >> 14] ? read_pointers_[0xbffc >> 14][0xbffc + page_id] : 0xff; } /// @returns The byte of RAM at @c address in the user memory map, if RAM is paged there. @c nullptr otherwise. uint8_t *user_ram(const uint8_t page, const uint16_t address) { // "User" accesses go to to wherever the user last had paged; // per 5.4 System Segment Usage those pages are stored in memory from // 0xbffc, so grab from there. const auto offset = address & 0x3fff; auto segment = ram_segment(page); if(segment) { return &segment[offset]; } return nullptr; } /// @returns The byte at @c address in the user memory map, whether ROM or RAM. uint8_t hostfs_user_read(const uint16_t address) override { const auto page = user_page(address); const auto ram = ram_segment(page); if(ram) return ram[address & 0x3fff]; const auto rom = rom_segment(page); if(rom) return rom[address & 0x3fff]; return 0xff; } /// Writes a byte to an address in the user memory map, if it's RAM. Otherwise acts as a no-op. void hostfs_user_write(const uint16_t address, const uint8_t value) override { const auto ram = user_ram(user_page(address), address); if(ram) *ram = value; } /// Searches @c host_fs_rom_ for high-level hooks and records those addresses into @c host_fs_traps_ with the assumption that the rom will /// be paged at 0xc000. Then covers up the hook with NOPs other than the first byte, which captures the hook code. void find_host_fs_hooks() { static constexpr uint8_t syscall[] = { 0xed, 0xfe, 0xfe }; auto begin = host_fs_rom_.begin(); while(true) { begin = std::search( begin, host_fs_rom_.end(), std::begin(syscall), std::end(syscall) ); if(begin == host_fs_rom_.end()) { break; } const auto offset = begin - host_fs_rom_.begin() + 0xc000; // ROM will be paged in slot 3, i.e. at $c000. host_fs_traps_.insert(uint16_t(offset)); // Move function code up to where this trap was, and NOP out the tail. begin[0] = begin[3]; begin[1] = begin[2] = begin[3] = 0x00; begin += 4; } } // MARK: - Activity Source void set_activity_observer([[maybe_unused]] Activity::Observer *const observer) final { if constexpr (has_disk_controller) { exdos_.set_activity_observer(observer); } } // MARK: - Configuration options. std::unique_ptr get_options() const final { auto options = std::make_unique(Configurable::OptionsType::UserFriendly); options->output = get_video_signal_configurable(); return options; } void set_options(const std::unique_ptr &str) final { const auto options = dynamic_cast(str.get()); set_video_signal_configurable(options->output); } }; } using namespace Enterprise; namespace { template std::unique_ptr machine( const Analyser::Static::Enterprise::Target &target, const ROMMachine::ROMFetcher &rom_fetcher ) { switch(target.model) { using enum Analyser::Static::Enterprise::Target::Model; default: __builtin_unreachable(); case Enterprise64: return std::make_unique> (target, rom_fetcher); case Enterprise128: return std::make_unique> (target, rom_fetcher); case Enterprise256: return std::make_unique> (target, rom_fetcher); } } template std::unique_ptr machine( const Analyser::Static::Enterprise::Target &target, const ROMMachine::ROMFetcher &rom_fetcher ) { if(target.speed == Analyser::Static::Enterprise::Target::Speed::SixMHz) { return machine(target, rom_fetcher); } else { return machine(target, rom_fetcher); } } } std::unique_ptr Machine::create( const Analyser::Static::Target &target, const ROMMachine::ROMFetcher &rom_fetcher ) { using Target = Analyser::Static::Enterprise::Target; const auto &enterprise_target = static_cast(target); if(enterprise_target.dos != Target::DOS::None) { return machine(enterprise_target, rom_fetcher); } else { return machine(enterprise_target, rom_fetcher); } }