// // MSX.cpp // Clock Signal // // Created by Thomas Harte on 24/11/2017. // Copyright 2017 Thomas Harte. All rights reserved. // #include "MSX.hpp" #include #include "DiskROM.hpp" #include "Keyboard.hpp" #include "MemorySlotHandler.hpp" #include "../../Analyser/Static/MSX/Cartridge.hpp" #include "Cartridges/ASCII8kb.hpp" #include "Cartridges/ASCII16kb.hpp" #include "Cartridges/Konami.hpp" #include "Cartridges/KonamiWithSCC.hpp" #include "../../Processors/Z80/Z80.hpp" #include "../../Components/1770/1770.hpp" #include "../../Components/8255/i8255.hpp" #include "../../Components/9918/9918.hpp" #include "../../Components/AudioToggle/AudioToggle.hpp" #include "../../Components/AY38910/AY38910.hpp" #include "../../Components/KonamiSCC/KonamiSCC.hpp" #include "../../Components/OPx/OPLL.hpp" #include "../../Components/RP5C01/RP5C01.hpp" #include "../../Storage/Tape/Parsers/MSX.hpp" #include "../../Storage/Tape/Tape.hpp" #include "../../Activity/Source.hpp" #include "../MachineTypes.hpp" #include "../../Configurable/Configurable.hpp" #include "../../Outputs/Log.hpp" #include "../../Outputs/Speaker/Implementation/CompoundSource.hpp" #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "../../Outputs/Speaker/Implementation/BufferSource.hpp" #include "../../Configurable/StandardOptions.hpp" #include "../../ClockReceiver/ForceInline.hpp" #include "../../ClockReceiver/JustInTime.hpp" #include "../../Analyser/Static/MSX/Target.hpp" namespace { Log::Logger logger; } namespace MSX { class AYPortHandler: public GI::AY38910::PortHandler { public: AYPortHandler(Storage::Tape::BinaryTapePlayer &tape_player) : tape_player_(tape_player) { joysticks_.emplace_back(new Joystick); joysticks_.emplace_back(new Joystick); } void set_port_output(bool port_b, uint8_t value) { if(port_b) { // Bits 0-3: touchpad handshaking (?) // Bit 4-5: monostable timer pulses // Bit 6: joystick select selected_joystick_ = (value >> 6) & 1; // Bit 7: code LED, if any } } uint8_t get_port_input(bool port_b) { if(!port_b) { // Bits 0-5: Joystick (up, down, left, right, A, B) // Bit 6: keyboard switch (not universal) // Bit 7: tape input return (static_cast(joysticks_[selected_joystick_].get())->get_state() & 0x3f) | 0x40 | (tape_player_.get_input() ? 0x00 : 0x80); } return 0xff; } const std::vector> &get_joysticks() { return joysticks_; } private: Storage::Tape::BinaryTapePlayer &tape_player_; std::vector> joysticks_; size_t selected_joystick_ = 0; class Joystick: public Inputs::ConcreteJoystick { public: Joystick() : ConcreteJoystick({ Input(Input::Up), Input(Input::Down), Input(Input::Left), Input(Input::Right), Input(Input::Fire, 0), Input(Input::Fire, 1), }) {} void did_set_input(const Input &input, bool is_active) final { uint8_t mask = 0; switch(input.type) { default: return; case Input::Up: mask = 0x01; break; case Input::Down: mask = 0x02; break; case Input::Left: mask = 0x04; break; case Input::Right: mask = 0x08; break; case Input::Fire: if(input.info.control.index >= 2) return; mask = input.info.control.index ? 0x20 : 0x10; break; } if(is_active) state_ &= ~mask; else state_ |= mask; } uint8_t get_state() { return state_; } private: uint8_t state_ = 0xff; }; }; template struct Speaker; template <> struct Speaker { Speaker() : ay(GI::AY38910::Personality::AY38910, audio_queue), audio_toggle(audio_queue), scc(audio_queue), mixer(ay, audio_toggle, scc), speaker(mixer) {} Concurrency::AsyncTaskQueue audio_queue; GI::AY38910::AY38910 ay; Audio::Toggle audio_toggle; Konami::SCC scc; using CompundSource = Outputs::Speaker::CompoundSource, Audio::Toggle, Konami::SCC>; CompundSource mixer; Outputs::Speaker::PullLowpass speaker; }; template <> struct Speaker { Speaker() : opll(audio_queue, 1), ay(GI::AY38910::Personality::AY38910, audio_queue), audio_toggle(audio_queue), scc(audio_queue), mixer(ay, audio_toggle, scc, opll), speaker(mixer) {} Concurrency::AsyncTaskQueue audio_queue; Yamaha::OPL::OPLL opll; GI::AY38910::AY38910 ay; Audio::Toggle audio_toggle; Konami::SCC scc; using CompundSource = Outputs::Speaker::CompoundSource, Audio::Toggle, Konami::SCC, Yamaha::OPL::OPLL>; CompundSource mixer; Outputs::Speaker::PullLowpass speaker; }; using Target = Analyser::Static::MSX::Target; template class ConcreteMachine: public Machine, public CPU::Z80::BusHandler, public MachineTypes::TimedMachine, public MachineTypes::AudioProducer, public MachineTypes::ScanProducer, public MachineTypes::MediaTarget, public MachineTypes::MappedKeyboardMachine, public MachineTypes::JoystickMachine, public Configurable::Device, public ClockingHint::Observer, public Activity::Source, public MSX::MemorySlotChangeHandler { private: // Provide 512kb of memory for an MSX 2; 64kb for an MSX 1. 'Slightly' arbitrary. static constexpr size_t RAMSize = model == Target::Model::MSX2 ? 512 * 1024 : 64 * 1024; static constexpr int ClockRate = 3579545; public: ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher): z80_(*this), i8255_(i8255_port_handler_), tape_player_(3579545 * 2), i8255_port_handler_(*this, speaker_.audio_toggle, tape_player_), ay_port_handler_(tape_player_), memory_slots_{{*this}, {*this}, {*this}, {*this}}, clock_(ClockRate) { set_clock_rate(ClockRate); clear_all_keys(); speaker_.ay.set_port_handler(&ay_port_handler_); speaker_.speaker.set_input_rate(3579545.0f / 2.0f); tape_player_.set_clocking_hint_observer(this); // Set the AY to 50% of available volume, the toggle to 10% and leave 40% for an SCC. // If there is an OPLL, give it equal volume to the AY and expect some clipping. if constexpr (has_opll) { speaker_.mixer.set_relative_volumes({0.5f, 0.1f, 0.4f, 0.5f}); } else { speaker_.mixer.set_relative_volumes({0.5f, 0.1f, 0.4f}); } // Install the proper TV standard and select an ideal BIOS name. const std::string machine_name = "MSX"; constexpr ROM::Name bios_name = model == Target::Model::MSX1 ? ROM::Name::MSXGenericBIOS : ROM::Name::MSX2GenericBIOS; ROM::Request bios_request = ROM::Request(bios_name); if constexpr (model == Target::Model::MSX2) { bios_request = bios_request && ROM::Request(ROM::Name::MSX2Extension); } bool is_ntsc = true; uint8_t character_generator = 1; /* 0 = Japan, 1 = USA, etc, 2 = USSR */ uint8_t date_format = 1; /* 0 = Y/M/D, 1 = M/D/Y, 2 = D/M/Y */ uint8_t keyboard = 1; /* 0 = Japan, 1 = USA, 2 = France, 3 = UK, 4 = Germany, 5 = USSR, 6 = Spain */ [[maybe_unused]] ROM::Name regional_bios_name; switch(target.region) { default: case Target::Region::Japan: if constexpr (model == Target::Model::MSX1) { regional_bios_name = ROM::Name::MSXJapaneseBIOS; } vdp_->set_tv_standard(TI::TMS::TVStandard::NTSC); is_ntsc = true; character_generator = 0; date_format = 0; break; case Target::Region::USA: if constexpr (model == Target::Model::MSX1) { regional_bios_name = ROM::Name::MSXAmericanBIOS; } vdp_->set_tv_standard(TI::TMS::TVStandard::NTSC); is_ntsc = true; character_generator = 1; date_format = 1; break; case Target::Region::Europe: if constexpr (model == Target::Model::MSX1) { regional_bios_name = ROM::Name::MSXEuropeanBIOS; } vdp_->set_tv_standard(TI::TMS::TVStandard::PAL); is_ntsc = false; character_generator = 1; date_format = 2; break; } if constexpr (model == Target::Model::MSX1) { bios_request = bios_request || ROM::Request(regional_bios_name); } // Fetch the necessary ROMs; try the region-specific ROM first, // but failing that fall back on patching the main one. ROM::Request request = bios_request; if(target.has_disk_drive) { request = request && ROM::Request(ROM::Name::MSXDOS); } if(target.has_msx_music) { request = request && ROM::Request(ROM::Name::MSXMusic); } auto roms = rom_fetcher(request); if(!request.validate(roms)) { throw ROMMachine::Error::MissingROMs; } // Figure out which BIOS to use, either a specific one or the generic // one appropriately patched. bool has_bios = false; if constexpr (model == Target::Model::MSX1) { const auto regional_bios = roms.find(regional_bios_name); if(regional_bios != roms.end()) { regional_bios->second.resize(32768); bios_slot().set_source(regional_bios->second); has_bios = true; } } if(!has_bios) { std::vector &bios = roms.find(bios_name)->second; bios.resize(32768); // Modify the generic ROM to reflect the selected region, date format, etc. bios[0x2b] = uint8_t( (is_ntsc ? 0x00 : 0x80) | (date_format << 4) | character_generator ); bios[0x2c] = keyboard; bios_slot().set_source(bios); } bios_slot().map(0, 0, 32768); ram_slot().resize_source(RAMSize); ram_slot().template map(0, 0, 65536); if constexpr (model == Target::Model::MSX2) { memory_slots_[3].supports_secondary_paging = true; const auto extension = roms.find(ROM::Name::MSX2Extension); extension->second.resize(32768); extension_rom_slot().set_source(extension->second); extension_rom_slot().map(0, 0, 32768); } // Add a disk cartridge if any disks were supplied. if(target.has_disk_drive) { disk_primary().handler = std::make_unique(disk_slot()); std::vector &dos = roms.find(ROM::Name::MSXDOS)->second; dos.resize(16384); disk_slot().set_source(dos); disk_slot().map(0, 0x4000, 0x2000); disk_slot().map_handler(0x6000, 0x2000); } // Grab the MSX-MUSIC ROM if applicable. if(target.has_msx_music) { std::vector &msx_music = roms.find(ROM::Name::MSXMusic)->second; msx_music.resize(65536); msx_music_slot().set_source(msx_music); msx_music_slot().map(0, 0, 0x10000); } // Insert the media. insert_media(target.media); // Type whatever has been requested. if(!target.loading_command.empty()) { type_string(target.loading_command); } // Establish default paging. page_primary(0); } ~ConcreteMachine() { speaker_.audio_queue.flush(); } void set_scan_target(Outputs::Display::ScanTarget *scan_target) final { vdp_->set_scan_target(scan_target); } Outputs::Display::ScanStatus get_scaled_scan_status() const final { return vdp_->get_scaled_scan_status(); } void set_display_type(Outputs::Display::DisplayType display_type) final { vdp_.last_valid()->set_display_type(display_type); } Outputs::Display::DisplayType get_display_type() const final { return vdp_.last_valid()->get_display_type(); } Outputs::Speaker::Speaker *get_speaker() final { return &speaker_.speaker; } void run_for(const Cycles cycles) final { z80_.run_for(cycles); } float get_confidence() final { if(performed_unmapped_access_ || pc_zero_accesses_ > 1) return 0.0f; if(cartridge_primary().handler) { return cartridge_primary().handler->get_confidence(); } return 0.5f; } std::string debug_type() final { if(cartridge_primary().handler) { return "MSX:" + cartridge_primary().handler->debug_type(); } return "MSX"; } bool insert_media(const Analyser::Static::Media &media) final { if(!media.cartridges.empty()) { const auto &segment = media.cartridges.front()->get_segments().front(); auto &slot = cartridge_slot(); slot.set_source(segment.data); slot.map(0, uint16_t(segment.start_address), std::min(segment.data.size(), 65536 - segment.start_address)); auto msx_cartridge = dynamic_cast(media.cartridges.front().get()); if(msx_cartridge) { switch(msx_cartridge->type) { default: break; case Analyser::Static::MSX::Cartridge::Konami: cartridge_primary().handler = std::make_unique(static_cast(slot)); break; case Analyser::Static::MSX::Cartridge::KonamiWithSCC: cartridge_primary().handler = std::make_unique(static_cast(slot), speaker_.scc); break; case Analyser::Static::MSX::Cartridge::ASCII8kb: cartridge_primary().handler = std::make_unique(static_cast(slot)); break; case Analyser::Static::MSX::Cartridge::ASCII16kb: cartridge_primary().handler = std::make_unique(static_cast(slot)); break; } } } if(!media.tapes.empty()) { tape_player_.set_tape(media.tapes.front()); } if(!media.disks.empty()) { DiskROM *const handler = disk_handler(); if(handler) { size_t drive = 0; for(auto &disk : media.disks) { handler->set_disk(disk, drive); drive++; if(drive == 2) break; } } } set_use_fast_tape(); return true; } void type_string(const std::string &string) final { std::transform( string.begin(), string.end(), std::back_inserter(input_text_), [](unsigned char c) -> unsigned char { return (c == '\n') ? '\r' : c; } ); } bool can_type(char c) const final { // Make an effort to type the entire printable ASCII range. return c >= 32 && c < 127; } // MARK: Memory paging. void page_primary(uint8_t value) { primary_slots_ = value; update_paging(); } void did_page() final { update_paging(); } void update_paging() { uint8_t primary = primary_slots_; // Update final slot; this direct pointer will be used for // secondary slot communication. final_slot_ = &memory_slots_[primary >> 6]; for(int c = 0; c < 8; c += 2) { const HandledSlot &slot = memory_slots_[primary & 3]; primary >>= 2; read_pointers_[c] = slot.read_pointer(c); write_pointers_[c] = slot.write_pointer(c); read_pointers_[c+1] = slot.read_pointer(c+1); write_pointers_[c+1] = slot.write_pointer(c+1); } set_use_fast_tape(); } // MARK: Z80::BusHandler forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) { // Per the best information I currently have, the MSX inserts an extra cycle into each opcode read, // but otherwise runs without pause. const HalfCycles addition((cycle.operation == CPU::Z80::PartialMachineCycle::ReadOpcode) ? 2 : 0); const HalfCycles total_length = addition + cycle.length; if(vdp_ += total_length) { z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun()); } time_since_ay_update_ += total_length; memory_slots_[0].cycles_since_update += total_length; memory_slots_[1].cycles_since_update += total_length; memory_slots_[2].cycles_since_update += total_length; memory_slots_[3].cycles_since_update += total_length; if constexpr (model >= Target::Model::MSX2) { clock_.run_for(total_length); } if(cycle.is_terminal()) { uint16_t address = cycle.address ? *cycle.address : 0x0000; switch(cycle.operation) { case CPU::Z80::PartialMachineCycle::ReadOpcode: if(use_fast_tape_) { if(address == 0x1a63) { // TAPION // Enable the tape motor. i8255_.write(0xab, 0x8); // Disable interrupts. z80_.set_value_of(CPU::Z80::Register::IFF1, 0); z80_.set_value_of(CPU::Z80::Register::IFF2, 0); // Use the parser to find a header, and if one is found then populate // LOWLIM and WINWID, and reset carry. Otherwise set carry. using Parser = Storage::Tape::MSX::Parser; std::unique_ptr new_speed = Parser::find_header(tape_player_); if(new_speed) { ram()[0xfca4] = new_speed->minimum_start_bit_duration; ram()[0xfca5] = new_speed->low_high_disrimination_duration; z80_.set_value_of(CPU::Z80::Register::Flags, 0); } else { z80_.set_value_of(CPU::Z80::Register::Flags, 1); } // RET. *cycle.value = 0xc9; break; } if(address == 0x1abc) { // TAPIN // Grab the current values of LOWLIM and WINWID. using Parser = Storage::Tape::MSX::Parser; Parser::FileSpeed tape_speed; tape_speed.minimum_start_bit_duration = ram()[0xfca4]; tape_speed.low_high_disrimination_duration = ram()[0xfca5]; // Ask the tape parser to grab a byte. int next_byte = Parser::get_byte(tape_speed, tape_player_); // If a byte was found, return it with carry unset. Otherwise set carry to // indicate error. if(next_byte >= 0) { z80_.set_value_of(CPU::Z80::Register::A, uint16_t(next_byte)); z80_.set_value_of(CPU::Z80::Register::Flags, 0); } else { z80_.set_value_of(CPU::Z80::Register::Flags, 1); } // RET. *cycle.value = 0xc9; break; } } if(!address) { pc_zero_accesses_++; } // TODO: below relates to confidence measurements. Reinstate, somehow. // if(is_unpopulated_[address >> 13] == unpopulated_) { // performed_unmapped_access_ = true; // } pc_address_ = address; // This is retained so as to be able to name the source of an access to cartridge handlers. [[fallthrough]]; case CPU::Z80::PartialMachineCycle::Read: if(address == 0xffff && final_slot_->supports_secondary_paging) { *cycle.value = final_slot_->secondary_paging() ^ 0xff; break; } if(read_pointers_[address >> 13]) { *cycle.value = read_pointers_[address >> 13][address & 8191]; } else { const int slot_hit = (primary_slots_ >> ((address >> 14) * 2)) & 3; memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.template flush()); *cycle.value = memory_slots_[slot_hit].handler->read(address); } break; case CPU::Z80::PartialMachineCycle::Write: { if(address == 0xffff && final_slot_->supports_secondary_paging) { final_slot_->set_secondary_paging(*cycle.value); update_paging(); break; } const int slot_hit = (primary_slots_ >> ((address >> 14) * 2)) & 3; if(memory_slots_[slot_hit].handler) { update_audio(); memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.template flush()); memory_slots_[slot_hit].handler->write( address, *cycle.value, read_pointers_[pc_address_ >> 13] != memory_slots_[0].read_pointer(pc_address_ >> 13)); } else { write_pointers_[address >> 13][address & 8191] = *cycle.value; } } break; case CPU::Z80::PartialMachineCycle::Input: switch(address & 0xff) { case 0x9a: case 0x9b: if constexpr (vdp_model() == TI::TMS::TMS9918A) { break; } [[fallthrough]]; case 0x98: case 0x99: *cycle.value = vdp_->read(address); z80_.set_interrupt_line(vdp_->get_interrupt_line()); break; case 0xa2: update_audio(); *cycle.value = GI::AY38910::Utility::read(speaker_.ay); break; case 0xa8: case 0xa9: case 0xaa: case 0xab: *cycle.value = i8255_.read(address); break; case 0xb5: if constexpr (model == Target::Model::MSX1) { break; } *cycle.value = clock_.read(next_clock_register_); break; case 0xfc: case 0xfd: case 0xfe: case 0xff: if constexpr (model != Target::Model::MSX1) { *cycle.value = ram_mapper_[(address & 0xff) - 0xfc]; break; } [[fallthrough]]; default: // printf("Unhandled read %02x\n", address & 0xff); *cycle.value = 0xff; break; } break; case CPU::Z80::PartialMachineCycle::Output: { const int port = address & 0xff; switch(port) { case 0x9a: case 0x9b: if constexpr (vdp_model() == TI::TMS::TMS9918A) { break; } [[fallthrough]]; case 0x98: case 0x99: vdp_->write(address, *cycle.value); z80_.set_interrupt_line(vdp_->get_interrupt_line()); break; case 0xa0: case 0xa1: update_audio(); GI::AY38910::Utility::write(speaker_.ay, port == 0xa1, *cycle.value); break; case 0xa8: case 0xa9: case 0xaa: case 0xab: i8255_.write(address, *cycle.value); break; case 0xb4: if constexpr (model == Target::Model::MSX1) { break; } next_clock_register_ = *cycle.value; break; case 0xb5: if constexpr (model == Target::Model::MSX1) { break; } clock_.write(next_clock_register_, *cycle.value); break; case 0xfc: case 0xfd: case 0xfe: case 0xff: { if constexpr (model == Target::Model::MSX1) { break; } ram_mapper_[port - 0xfc] = *cycle.value; // Apply to RAM. // // On a real MSX this may also affect other slots. // I've not yet needed it to propagate further, so // have not implemented any onward route. const uint16_t region = uint16_t((port - 0xfc) << 14); const size_t base = size_t(*cycle.value) << 14; if(base < RAMSize) { ram_slot().template map(base, region, 0x4000); } else { ram_slot().unmap(region, 0x4000); } update_paging(); } break; case 0x7c: case 0x7d: if constexpr (has_opll) { speaker_.opll.write(address, *cycle.value); break; } [[fallthrough]]; default: printf("Unhandled write %02x of %02x\n", address & 0xff, *cycle.value); break; } } break; case CPU::Z80::PartialMachineCycle::Interrupt: *cycle.value = 0xff; // Take this as a convenient moment to jump into the keyboard buffer, if desired. if(!input_text_.empty()) { // The following are KEYBUF per the Red Book; its address and its definition as DEFS 40. const int buffer_start = 0xfbf0; const int buffer_size = 40; // Also from the Red Book: GETPNT is at F3FAH and PUTPNT is at F3F8H. int read_address = ram()[0xf3fa] | (ram()[0xf3fb] << 8); int write_address = ram()[0xf3f8] | (ram()[0xf3f9] << 8); // Write until either the string is exhausted or the write_pointer is immediately // behind the read pointer; temporarily map write_address and read_address into // buffer-relative values. std::size_t characters_written = 0; write_address -= buffer_start; read_address -= buffer_start; while(characters_written < input_text_.size()) { const int next_write_address = (write_address + 1) % buffer_size; if(next_write_address == read_address) break; ram()[write_address + buffer_start] = uint8_t(input_text_[characters_written]); ++characters_written; write_address = next_write_address; } input_text_.erase(input_text_.begin(), input_text_.begin() + std::string::difference_type(characters_written)); // Map the write address back into absolute terms and write it out again as PUTPNT. write_address += buffer_start; ram()[0xf3f8] = uint8_t(write_address); ram()[0xf3f9] = uint8_t(write_address >> 8); } break; default: break; } } if(!tape_player_is_sleeping_) tape_player_.run_for(int(cycle.length.as_integral())); return addition; } void flush_output(int outputs) final { if(outputs & Output::Video) { vdp_.flush(); } if(outputs & Output::Audio) { update_audio(); speaker_.audio_queue.perform(); } } void set_keyboard_line(int line) { selected_key_line_ = line; } uint8_t read_keyboard() { return key_states_[selected_key_line_]; } void clear_all_keys() final { std::memset(key_states_, 0xff, sizeof(key_states_)); } void set_key_state(uint16_t key, bool is_pressed) final { const int mask = 1 << (key & 7); const int line = key >> 4; if(is_pressed) key_states_[line] &= ~mask; else key_states_[line] |= mask; } KeyboardMapper *get_keyboard_mapper() final { return &keyboard_mapper_; } // MARK: - Configuration options. std::unique_ptr get_options() final { auto options = std::make_unique(Configurable::OptionsType::UserFriendly); options->output = get_video_signal_configurable(); options->quickload = allow_fast_tape_; return options; } void set_options(const std::unique_ptr &str) final { const auto options = dynamic_cast(str.get()); set_video_signal_configurable(options->output); allow_fast_tape_ = options->quickload; set_use_fast_tape(); } // MARK: - Sleeper void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) final { tape_player_is_sleeping_ = tape_player_.preferred_clocking() == ClockingHint::Preference::None; set_use_fast_tape(); } // MARK: - Activity::Source void set_activity_observer(Activity::Observer *observer) final { DiskROM *const handler = disk_handler(); if(handler) { handler->set_activity_observer(observer); } i8255_port_handler_.set_activity_observer(observer); } // MARK: - Joysticks const std::vector> &get_joysticks() final { return ay_port_handler_.get_joysticks(); } private: void update_audio() { speaker_.speaker.run_for(speaker_.audio_queue, time_since_ay_update_.divide_cycles(Cycles(2))); } class i8255PortHandler: public Intel::i8255::PortHandler { public: i8255PortHandler(ConcreteMachine &machine, Audio::Toggle &audio_toggle, Storage::Tape::BinaryTapePlayer &tape_player) : machine_(machine), audio_toggle_(audio_toggle), tape_player_(tape_player) {} void set_value(int port, uint8_t value) { switch(port) { case 0: machine_.page_primary(value); break; case 2: { // TODO: // b6 caps lock LED // b5 audio output // b4: cassette motor relay tape_player_.set_motor_control(!(value & 0x10)); if(activity_observer_) activity_observer_->set_led_status("Tape motor", !(value & 0x10)); // b7: keyboard click bool new_audio_level = !!(value & 0x80); if(audio_toggle_.get_output() != new_audio_level) { machine_.update_audio(); audio_toggle_.set_output(new_audio_level); } // b0-b3: keyboard line machine_.set_keyboard_line(value & 0xf); } break; default: logger.error().append("Unrecognised: MSX set 8255 output port %d to value %02x", port, value); break; } } uint8_t get_value(int port) { if(port == 1) { return machine_.read_keyboard(); } else logger.error().append("MSX attempted to read from 8255 port %d"); return 0xff; } void set_activity_observer(Activity::Observer *observer) { activity_observer_ = observer; if(activity_observer_) { activity_observer_->register_led("Tape motor"); activity_observer_->set_led_status("Tape motor", tape_player_.get_motor_control()); } } private: ConcreteMachine &machine_; Audio::Toggle &audio_toggle_; Storage::Tape::BinaryTapePlayer &tape_player_; Activity::Observer *activity_observer_ = nullptr; }; static constexpr TI::TMS::Personality vdp_model() { switch(model) { case Target::Model::MSX1: return TI::TMS::Personality::TMS9918A; case Target::Model::MSX2: return TI::TMS::Personality::V9938; } } CPU::Z80::Processor z80_; JustInTimeActor> vdp_; Intel::i8255::i8255 i8255_; Storage::Tape::BinaryTapePlayer tape_player_; bool tape_player_is_sleeping_ = false; bool allow_fast_tape_ = false; bool use_fast_tape_ = false; void set_use_fast_tape() { use_fast_tape_ = !tape_player_is_sleeping_ && allow_fast_tape_ && tape_player_.has_tape() && !(primary_slots_ & 3) && !(memory_slots_[0].secondary_paging() & 3); } i8255PortHandler i8255_port_handler_; Speaker speaker_; AYPortHandler ay_port_handler_; /// The current primary and secondary slot selections; the former retains whatever was written /// last to the 8255 PPI via port A8 and the latter — if enabled — captures 0xffff on a per-slot basis. uint8_t primary_slots_ = 0; // Divides the current 64kb address space into 8kb chunks. // 8kb resolution is used by some cartride titles. const uint8_t *read_pointers_[8]; uint8_t *write_pointers_[8]; uint8_t ram_mapper_[4]{}; /// Optionally attaches non-default logic to any of the four things selectable /// via the primary slot register. /// /// In principle one might want to attach a handler to a secondary slot rather /// than a primary, but in practice that isn't required in the slot allocation used /// by this emulator. struct HandledSlot: public MSX::PrimarySlot { using MSX::PrimarySlot::PrimarySlot; /// Storage for a slot-specialised handler. std::unique_ptr handler; /// The handler is updated just-in-time. HalfCycles cycles_since_update; }; HandledSlot memory_slots_[4]; HandledSlot *final_slot_ = nullptr; HalfCycles time_since_ay_update_; uint8_t key_states_[16]; int selected_key_line_ = 0; std::string input_text_; MSX::KeyboardMapper keyboard_mapper_; int pc_zero_accesses_ = 0; bool performed_unmapped_access_ = false; uint16_t pc_address_; Ricoh::RP5C01::RP5C01 clock_; int next_clock_register_ = 0; // // Various helpers that dictate the slot arrangement used by this emulator. // // That arrangement is: // // Slot 0 is the BIOS, and does not support secondary paging. // Slot 1 holds a [game, probably] cartridge, if inserted. No secondary paging. // Slot 2 holds the disk cartridge, if inserted. // // On an MSX 1, Slot 3 holds 64kb of RAM. // // On an MSX 2: // // Slot 3-0 holds a larger amount of RAM (cf. RAMSize) that is subject to the // FC-FF paging selections. // // Slot 3-1 holds the BIOS extension ROM. // // Slot 3-2 holds the MSX-MUSIC. // MemorySlot &bios_slot() { return memory_slots_[0].subslot(0); } MemorySlot &ram_slot() { return memory_slots_[3].subslot(0); } MemorySlot &extension_rom_slot() { return memory_slots_[3].subslot(1); } MemorySlot &msx_music_slot() { return memory_slots_[3].subslot(2); } MemorySlot &cartridge_slot() { return cartridge_primary().subslot(0); } MemorySlot &disk_slot() { return disk_primary().subslot(0); } HandledSlot &cartridge_primary() { return memory_slots_[1]; } HandledSlot &disk_primary() { return memory_slots_[2]; } uint8_t *ram() { return ram_slot().source().data(); } DiskROM *disk_handler() { return dynamic_cast(disk_primary().handler.get()); } }; } using namespace MSX; std::unique_ptr Machine::MSX(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { const auto msx_target = dynamic_cast(target); if(msx_target->has_msx_music) { switch(msx_target->model) { default: return nullptr; case Target::Model::MSX1: return std::make_unique>(*msx_target, rom_fetcher); case Target::Model::MSX2: return std::make_unique>(*msx_target, rom_fetcher); } } else { switch(msx_target->model) { default: return nullptr; case Target::Model::MSX1: return std::make_unique>(*msx_target, rom_fetcher); case Target::Model::MSX2: return std::make_unique>(*msx_target, rom_fetcher); } } }