2018-09-21 02:04:28 +00:00
|
|
|
|
//
|
|
|
|
|
// MasterSystem.cpp
|
|
|
|
|
// Clock Signal
|
|
|
|
|
//
|
|
|
|
|
// Created by Thomas Harte on 20/09/2018.
|
|
|
|
|
// Copyright © 2018 Thomas Harte. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
#include "MasterSystem.hpp"
|
2018-09-22 02:13:07 +00:00
|
|
|
|
|
|
|
|
|
#include "../../Processors/Z80/Z80.hpp"
|
|
|
|
|
|
|
|
|
|
#include "../../Components/9918/9918.hpp"
|
|
|
|
|
#include "../../Components/SN76489/SN76489.hpp"
|
2020-05-09 22:04:11 +00:00
|
|
|
|
#include "../../Components/OPx/OPLL.hpp"
|
2018-09-22 02:13:07 +00:00
|
|
|
|
|
2020-04-02 03:19:34 +00:00
|
|
|
|
#include "../MachineTypes.hpp"
|
2020-03-16 03:48:53 +00:00
|
|
|
|
#include "../../Configurable/Configurable.hpp"
|
2018-09-22 02:13:07 +00:00
|
|
|
|
|
|
|
|
|
#include "../../ClockReceiver/ForceInline.hpp"
|
2019-07-29 19:38:41 +00:00
|
|
|
|
#include "../../ClockReceiver/JustInTime.hpp"
|
2018-09-22 02:13:07 +00:00
|
|
|
|
|
|
|
|
|
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
2020-04-04 00:05:36 +00:00
|
|
|
|
#include "../../Outputs/Speaker/Implementation/CompoundSource.hpp"
|
2020-02-10 00:12:44 +00:00
|
|
|
|
|
2018-10-19 01:16:56 +00:00
|
|
|
|
#include "../../Outputs/Log.hpp"
|
2018-09-22 02:13:07 +00:00
|
|
|
|
|
2018-09-23 20:34:47 +00:00
|
|
|
|
#include "../../Analyser/Static/Sega/Target.hpp"
|
|
|
|
|
|
2018-09-25 01:34:42 +00:00
|
|
|
|
#include <algorithm>
|
2023-01-01 02:54:14 +00:00
|
|
|
|
#include <cassert>
|
2018-10-27 01:19:16 +00:00
|
|
|
|
#include <iostream>
|
2018-09-25 01:34:42 +00:00
|
|
|
|
|
2018-09-22 02:13:07 +00:00
|
|
|
|
namespace {
|
2020-04-25 03:50:06 +00:00
|
|
|
|
constexpr int audio_divider = 1;
|
2024-01-20 03:02:26 +00:00
|
|
|
|
Log::Logger<Log::Source::MasterSystem> logger;
|
2018-09-22 02:13:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace Sega {
|
|
|
|
|
namespace MasterSystem {
|
|
|
|
|
|
2018-09-24 01:55:07 +00:00
|
|
|
|
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)
|
|
|
|
|
}) {}
|
|
|
|
|
|
2020-01-24 03:57:51 +00:00
|
|
|
|
void did_set_input(const Input &digital_input, bool is_active) final {
|
2018-09-24 01:55:07 +00:00
|
|
|
|
switch(digital_input.type) {
|
|
|
|
|
default: return;
|
|
|
|
|
|
2018-11-24 03:32:32 +00:00
|
|
|
|
case Input::Up: if(is_active) state_ &= ~0x01; else state_ |= 0x01; break;
|
2018-09-24 01:55:07 +00:00
|
|
|
|
case Input::Down: if(is_active) state_ &= ~0x02; else state_ |= 0x02; break;
|
|
|
|
|
case Input::Left: if(is_active) state_ &= ~0x04; else state_ |= 0x04; break;
|
|
|
|
|
case Input::Right: if(is_active) state_ &= ~0x08; else state_ |= 0x08; break;
|
|
|
|
|
case Input::Fire:
|
|
|
|
|
switch(digital_input.info.control.index) {
|
|
|
|
|
default: break;
|
|
|
|
|
case 0: if(is_active) state_ &= ~0x10; else state_ |= 0x10; break;
|
|
|
|
|
case 1: if(is_active) state_ &= ~0x20; else state_ |= 0x20; break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint8_t get_state() {
|
|
|
|
|
return state_;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
uint8_t state_ = 0xff;
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-01 02:54:14 +00:00
|
|
|
|
template <Analyser::Static::Sega::Target::Model model> class ConcreteMachine:
|
2018-09-22 02:13:07 +00:00
|
|
|
|
public Machine,
|
|
|
|
|
public CPU::Z80::BusHandler,
|
2020-04-02 03:19:34 +00:00
|
|
|
|
public MachineTypes::TimedMachine,
|
|
|
|
|
public MachineTypes::ScanProducer,
|
|
|
|
|
public MachineTypes::AudioProducer,
|
|
|
|
|
public MachineTypes::KeyboardMachine,
|
|
|
|
|
public MachineTypes::JoystickMachine,
|
2018-10-24 02:30:24 +00:00
|
|
|
|
public Configurable::Device,
|
2020-04-02 03:19:34 +00:00
|
|
|
|
public Inputs::Keyboard::Delegate {
|
2018-09-22 02:13:07 +00:00
|
|
|
|
|
|
|
|
|
public:
|
2018-09-23 20:34:47 +00:00
|
|
|
|
ConcreteMachine(const Analyser::Static::Sega::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
2018-10-20 01:37:05 +00:00
|
|
|
|
region_(target.region),
|
|
|
|
|
paging_scheme_(target.paging_scheme),
|
2018-09-22 02:13:07 +00:00
|
|
|
|
z80_(*this),
|
2018-09-24 02:24:29 +00:00
|
|
|
|
sn76489_(
|
2018-10-20 01:37:05 +00:00
|
|
|
|
(target.model == Target::Model::SG1000) ? TI::SN76489::Personality::SN76489 : TI::SN76489::Personality::SMS,
|
2018-09-24 02:24:29 +00:00
|
|
|
|
audio_queue_,
|
2020-04-12 16:46:40 +00:00
|
|
|
|
audio_divider),
|
|
|
|
|
opll_(audio_queue_, audio_divider),
|
2020-04-04 00:05:36 +00:00
|
|
|
|
mixer_(sn76489_, opll_),
|
|
|
|
|
speaker_(mixer_),
|
2019-09-22 17:48:50 +00:00
|
|
|
|
keyboard_({Inputs::Keyboard::Key::Enter, Inputs::Keyboard::Key::Escape}, {}) {
|
2018-10-20 01:37:05 +00:00
|
|
|
|
// Pick the clock rate based on the region.
|
|
|
|
|
const double clock_rate = target.region == Target::Region::Europe ? 3546893.0 : 3579540.0;
|
2020-05-10 03:00:39 +00:00
|
|
|
|
speaker_.set_input_rate(float(clock_rate / audio_divider));
|
2018-10-20 01:37:05 +00:00
|
|
|
|
set_clock_rate(clock_rate);
|
2018-09-23 03:45:29 +00:00
|
|
|
|
|
2018-09-24 01:55:07 +00:00
|
|
|
|
// Instantiate the joysticks.
|
|
|
|
|
joysticks_.emplace_back(new Joystick);
|
|
|
|
|
joysticks_.emplace_back(new Joystick);
|
|
|
|
|
|
|
|
|
|
// Clear the memory map.
|
2018-09-25 01:34:42 +00:00
|
|
|
|
map(read_pointers_, nullptr, 0x10000, 0);
|
|
|
|
|
map(write_pointers_, nullptr, 0x10000, 0);
|
2018-09-23 21:36:30 +00:00
|
|
|
|
|
2018-09-23 21:42:42 +00:00
|
|
|
|
// Take a copy of the cartridge and place it into memory.
|
2020-04-30 02:07:20 +00:00
|
|
|
|
if(!target.media.cartridges.empty()) {
|
|
|
|
|
cartridge_ = target.media.cartridges[0]->get_segments()[0].data;
|
|
|
|
|
}
|
2018-09-25 01:34:42 +00:00
|
|
|
|
if(cartridge_.size() < 48*1024) {
|
|
|
|
|
std::size_t new_space = 48*1024 - cartridge_.size();
|
|
|
|
|
cartridge_.resize(48*1024);
|
|
|
|
|
memset(&cartridge_[48*1024 - new_space], 0xff, new_space);
|
|
|
|
|
}
|
2018-10-20 02:10:14 +00:00
|
|
|
|
|
|
|
|
|
if(paging_scheme_ == Target::PagingScheme::Codemasters) {
|
|
|
|
|
// The Codemasters cartridges start with pages 0, 1 and 0 again initially visible.
|
|
|
|
|
paging_registers_[0] = 0;
|
|
|
|
|
paging_registers_[1] = 1;
|
|
|
|
|
paging_registers_[2] = 0;
|
|
|
|
|
}
|
2018-09-23 21:42:42 +00:00
|
|
|
|
|
2020-05-12 04:11:46 +00:00
|
|
|
|
// Load the BIOS if available.
|
|
|
|
|
//
|
|
|
|
|
// TODO: there's probably a million other versions of the Master System BIOS; try to build a
|
|
|
|
|
// CRC32 catalogue of those. So far:
|
|
|
|
|
//
|
|
|
|
|
// 0072ed54 = US/European BIOS 1.3
|
|
|
|
|
// 48d44a13 = Japanese BIOS 2.1
|
|
|
|
|
const bool is_japanese = target.region == Target::Region::Japan;
|
2021-06-04 01:55:59 +00:00
|
|
|
|
const ROM::Name bios_name = is_japanese ? ROM::Name::MasterSystemJapaneseBIOS : ROM::Name::MasterSystemWesternBIOS;
|
|
|
|
|
ROM::Request request(bios_name, true);
|
2021-06-04 22:54:50 +00:00
|
|
|
|
auto roms = rom_fetcher(request);
|
|
|
|
|
request.validate(roms);
|
2021-06-04 01:55:59 +00:00
|
|
|
|
|
|
|
|
|
const auto rom = roms.find(bios_name);
|
|
|
|
|
if(rom == roms.end()) {
|
2020-05-12 04:11:46 +00:00
|
|
|
|
// No BIOS found; attempt to boot as though it has already disabled itself.
|
|
|
|
|
has_bios_ = false;
|
|
|
|
|
memory_control_ |= 0x08;
|
|
|
|
|
std::cerr << "No BIOS found; attempting to start cartridge directly" << std::endl;
|
|
|
|
|
} else {
|
|
|
|
|
has_bios_ = true;
|
2021-06-04 01:55:59 +00:00
|
|
|
|
memcpy(&bios_, rom->second.data(), std::min(sizeof(bios_), rom->second.size()));
|
2018-10-20 01:37:05 +00:00
|
|
|
|
}
|
2020-03-24 00:00:31 +00:00
|
|
|
|
page_cartridge();
|
2018-09-23 21:36:30 +00:00
|
|
|
|
|
2018-10-20 01:37:05 +00:00
|
|
|
|
// Map RAM.
|
2023-01-01 02:54:14 +00:00
|
|
|
|
if constexpr (is_master_system(model)) {
|
2018-09-23 21:36:30 +00:00
|
|
|
|
map(read_pointers_, ram_, 8*1024, 0xc000, 0x10000);
|
|
|
|
|
map(write_pointers_, ram_, 8*1024, 0xc000, 0x10000);
|
|
|
|
|
} else {
|
|
|
|
|
map(read_pointers_, ram_, 1024, 0xc000, 0x10000);
|
|
|
|
|
map(write_pointers_, ram_, 1024, 0xc000, 0x10000);
|
|
|
|
|
}
|
2018-10-07 22:39:03 +00:00
|
|
|
|
|
2020-04-21 23:57:13 +00:00
|
|
|
|
// Apply a relatively low low-pass filter. More guidance needed here.
|
2020-04-30 02:07:20 +00:00
|
|
|
|
// TODO: this is disabled for now since it isn't applicable for the FM chip, I think.
|
2020-04-21 23:57:13 +00:00
|
|
|
|
// speaker_.set_high_frequency_cutoff(8000);
|
|
|
|
|
|
|
|
|
|
// Set default mixer levels: FM off, SN full-throttle.
|
|
|
|
|
set_mixer_levels(0);
|
2018-10-25 01:59:30 +00:00
|
|
|
|
|
|
|
|
|
keyboard_.set_delegate(this);
|
2018-09-22 02:13:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~ConcreteMachine() {
|
|
|
|
|
audio_queue_.flush();
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 03:57:51 +00:00
|
|
|
|
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
2023-03-11 02:04:55 +00:00
|
|
|
|
vdp_.last_valid()->set_tv_standard(
|
2018-10-20 01:37:05 +00:00
|
|
|
|
(region_ == Target::Region::Europe) ?
|
2018-10-20 02:37:56 +00:00
|
|
|
|
TI::TMS::TVStandard::PAL : TI::TMS::TVStandard::NTSC);
|
2018-11-16 02:21:54 +00:00
|
|
|
|
|
2023-03-11 02:04:55 +00:00
|
|
|
|
// Doing the following would be technically correct, but isn't
|
|
|
|
|
// especially thread-safe and won't make a substantial difference.
|
|
|
|
|
// time_until_debounce_ = vdp_->get_time_until_line(-1);
|
|
|
|
|
|
|
|
|
|
vdp_.last_valid()->set_scan_target(scan_target);
|
2018-09-22 02:13:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-22 03:28:25 +00:00
|
|
|
|
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
2023-03-11 02:04:55 +00:00
|
|
|
|
return vdp_.last_valid()->get_scaled_scan_status();
|
2020-01-21 02:45:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 03:57:51 +00:00
|
|
|
|
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
2023-03-11 02:04:55 +00:00
|
|
|
|
vdp_.last_valid()->set_display_type(display_type);
|
2018-11-30 04:44:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-21 03:34:26 +00:00
|
|
|
|
Outputs::Display::DisplayType get_display_type() const final {
|
2023-03-11 02:04:55 +00:00
|
|
|
|
return vdp_.last_valid()->get_display_type();
|
2020-03-18 22:26:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 03:57:51 +00:00
|
|
|
|
Outputs::Speaker::Speaker *get_speaker() final {
|
2018-09-22 02:13:07 +00:00
|
|
|
|
return &speaker_;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 03:57:51 +00:00
|
|
|
|
void run_for(const Cycles cycles) final {
|
2018-09-22 02:13:07 +00:00
|
|
|
|
z80_.run_for(cycles);
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-09 17:33:46 +00:00
|
|
|
|
void flush_output(int outputs) final {
|
|
|
|
|
if(outputs & Output::Video) {
|
2022-07-08 19:38:29 +00:00
|
|
|
|
vdp_.flush();
|
|
|
|
|
}
|
2022-07-09 17:33:46 +00:00
|
|
|
|
if(outputs & Output::Audio) {
|
2022-07-08 19:38:29 +00:00
|
|
|
|
update_audio();
|
|
|
|
|
audio_queue_.perform();
|
|
|
|
|
}
|
2022-07-07 20:46:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 02:13:07 +00:00
|
|
|
|
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
2021-04-06 01:02:37 +00:00
|
|
|
|
if(vdp_ += cycle.length) {
|
|
|
|
|
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
|
|
|
|
|
}
|
2018-09-22 02:13:07 +00:00
|
|
|
|
time_since_sn76489_update_ += cycle.length;
|
|
|
|
|
|
2018-09-23 03:45:29 +00:00
|
|
|
|
if(cycle.is_terminal()) {
|
|
|
|
|
uint16_t address = cycle.address ? *cycle.address : 0x0000;
|
|
|
|
|
switch(cycle.operation) {
|
|
|
|
|
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
|
|
|
|
case CPU::Z80::PartialMachineCycle::Read:
|
2018-09-23 21:36:30 +00:00
|
|
|
|
*cycle.value = read_pointers_[address >> 10] ? read_pointers_[address >> 10][address & 1023] : 0xff;
|
2018-09-23 03:45:29 +00:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CPU::Z80::PartialMachineCycle::Write:
|
2018-10-20 02:10:14 +00:00
|
|
|
|
if(paging_scheme_ == Target::PagingScheme::Sega) {
|
|
|
|
|
if(address >= 0xfffd && cartridge_.size() > 48*1024) {
|
|
|
|
|
if(paging_registers_[address - 0xfffd] != *cycle.value) {
|
|
|
|
|
paging_registers_[address - 0xfffd] = *cycle.value;
|
|
|
|
|
page_cartridge();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// i.e. this is the Codemasters paging scheme.
|
|
|
|
|
if(!(address&0x3fff) && address < 0xc000) {
|
|
|
|
|
if(paging_registers_[address >> 14] != *cycle.value) {
|
|
|
|
|
paging_registers_[address >> 14] = *cycle.value;
|
|
|
|
|
page_cartridge();
|
|
|
|
|
}
|
2018-10-05 02:50:35 +00:00
|
|
|
|
}
|
2018-09-25 01:34:42 +00:00
|
|
|
|
}
|
2018-10-06 18:26:00 +00:00
|
|
|
|
|
|
|
|
|
if(write_pointers_[address >> 10]) write_pointers_[address >> 10][address & 1023] = *cycle.value;
|
2024-01-20 03:02:26 +00:00
|
|
|
|
// else logger.info().append("Ignored write to ROM");
|
2018-09-23 03:45:29 +00:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CPU::Z80::PartialMachineCycle::Input:
|
2018-09-23 20:05:37 +00:00
|
|
|
|
switch(address & 0xc1) {
|
|
|
|
|
case 0x00:
|
2024-01-20 03:02:26 +00:00
|
|
|
|
logger.error().append("TODO: [input] memory control");
|
2018-09-23 20:34:47 +00:00
|
|
|
|
*cycle.value = 0xff;
|
2018-09-23 20:05:37 +00:00
|
|
|
|
break;
|
|
|
|
|
case 0x01:
|
2024-01-20 03:02:26 +00:00
|
|
|
|
logger.error().append("TODO: [input] I/O port control");
|
2018-09-23 20:34:47 +00:00
|
|
|
|
*cycle.value = 0xff;
|
2018-09-23 20:05:37 +00:00
|
|
|
|
break;
|
2018-10-11 23:56:32 +00:00
|
|
|
|
case 0x40:
|
2019-07-29 19:38:41 +00:00
|
|
|
|
*cycle.value = vdp_->get_current_line();
|
2018-09-23 20:05:37 +00:00
|
|
|
|
break;
|
2018-10-11 23:56:32 +00:00
|
|
|
|
case 0x41:
|
2019-07-29 21:17:04 +00:00
|
|
|
|
*cycle.value = vdp_.last_valid()->get_latched_horizontal_counter();
|
2018-10-11 23:56:32 +00:00
|
|
|
|
break;
|
2018-09-23 20:05:37 +00:00
|
|
|
|
case 0x80: case 0x81:
|
2020-01-05 18:40:02 +00:00
|
|
|
|
*cycle.value = vdp_->read(address);
|
2019-07-29 19:38:41 +00:00
|
|
|
|
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
2018-09-23 20:05:37 +00:00
|
|
|
|
break;
|
2018-09-24 01:55:07 +00:00
|
|
|
|
case 0xc0: {
|
2020-04-04 00:05:36 +00:00
|
|
|
|
if(memory_control_ & 0x4) {
|
|
|
|
|
if(has_fm_audio_ && (address & 0xff) == 0xf2) {
|
|
|
|
|
*cycle.value = opll_detection_word_;
|
|
|
|
|
} else {
|
|
|
|
|
*cycle.value = 0xff;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Joystick *const joypad1 = static_cast<Joystick *>(joysticks_[0].get());
|
|
|
|
|
Joystick *const joypad2 = static_cast<Joystick *>(joysticks_[1].get());
|
2020-05-10 03:00:39 +00:00
|
|
|
|
*cycle.value = uint8_t(joypad1->get_state() | (joypad2->get_state() << 6));
|
2020-04-04 00:05:36 +00:00
|
|
|
|
}
|
2018-09-24 01:55:07 +00:00
|
|
|
|
} break;
|
|
|
|
|
case 0xc1: {
|
2020-04-04 00:05:36 +00:00
|
|
|
|
if(memory_control_ & 0x4) {
|
|
|
|
|
*cycle.value = 0xff;
|
|
|
|
|
} else {
|
|
|
|
|
Joystick *const joypad2 = static_cast<Joystick *>(joysticks_[1].get());
|
|
|
|
|
|
|
|
|
|
*cycle.value =
|
|
|
|
|
(joypad2->get_state() >> 2) |
|
|
|
|
|
0x30 |
|
|
|
|
|
get_th_values();
|
|
|
|
|
}
|
2018-09-24 01:55:07 +00:00
|
|
|
|
} break;
|
2018-09-23 20:05:37 +00:00
|
|
|
|
|
|
|
|
|
default:
|
2024-01-20 03:02:26 +00:00
|
|
|
|
logger.error().append("[input] Clearly some sort of typo");
|
2018-09-23 20:05:37 +00:00
|
|
|
|
break;
|
|
|
|
|
}
|
2018-09-23 03:45:29 +00:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CPU::Z80::PartialMachineCycle::Output:
|
|
|
|
|
switch(address & 0xc1) {
|
2020-04-04 00:05:36 +00:00
|
|
|
|
case 0x00: // i.e. even ports less than 0x40.
|
2023-01-01 02:54:14 +00:00
|
|
|
|
if constexpr (is_master_system(model)) {
|
2018-09-25 01:34:42 +00:00
|
|
|
|
// TODO: Obey the RAM enable.
|
2024-01-20 03:02:26 +00:00
|
|
|
|
logger.info().append("Memory control: %02x", memory_control_);
|
2018-10-06 18:26:00 +00:00
|
|
|
|
memory_control_ = *cycle.value;
|
|
|
|
|
page_cartridge();
|
2018-09-25 01:34:42 +00:00
|
|
|
|
}
|
2018-09-23 03:45:29 +00:00
|
|
|
|
break;
|
2020-04-04 00:05:36 +00:00
|
|
|
|
case 0x01: { // i.e. odd ports less than 0x40.
|
2018-10-11 23:56:32 +00:00
|
|
|
|
// A programmer can force the TH lines to 0 here,
|
|
|
|
|
// causing a phoney lightgun latch, so check for any
|
|
|
|
|
// discontinuity in TH inputs.
|
|
|
|
|
const auto previous_ths = get_th_values();
|
|
|
|
|
io_port_control_ = *cycle.value;
|
|
|
|
|
const auto new_ths = get_th_values();
|
|
|
|
|
|
|
|
|
|
// Latch if either TH has newly gone to 1.
|
|
|
|
|
if((new_ths^previous_ths)&new_ths) {
|
2019-07-29 19:38:41 +00:00
|
|
|
|
vdp_->latch_horizontal_counter();
|
2018-10-11 23:56:32 +00:00
|
|
|
|
}
|
|
|
|
|
} break;
|
2020-04-04 00:05:36 +00:00
|
|
|
|
case 0x40: case 0x41: // i.e. ports 0x40–0x7f.
|
2018-09-23 03:45:29 +00:00
|
|
|
|
update_audio();
|
2020-01-05 18:40:02 +00:00
|
|
|
|
sn76489_.write(*cycle.value);
|
2018-09-23 03:45:29 +00:00
|
|
|
|
break;
|
2020-04-04 00:05:36 +00:00
|
|
|
|
case 0x80: case 0x81: // i.e. ports 0x80–0xbf.
|
2020-01-05 18:40:02 +00:00
|
|
|
|
vdp_->write(address, *cycle.value);
|
2019-07-29 19:38:41 +00:00
|
|
|
|
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
2018-09-23 03:45:29 +00:00
|
|
|
|
break;
|
2020-04-04 00:05:36 +00:00
|
|
|
|
case 0xc1: case 0xc0: // i.e. ports 0xc0–0xff.
|
|
|
|
|
if(has_fm_audio_) {
|
|
|
|
|
switch(address & 0xff) {
|
|
|
|
|
case 0xf0: case 0xf1:
|
2020-04-12 16:46:40 +00:00
|
|
|
|
update_audio();
|
2020-04-04 00:05:36 +00:00
|
|
|
|
opll_.write(address, *cycle.value);
|
|
|
|
|
break;
|
|
|
|
|
case 0xf2:
|
|
|
|
|
opll_detection_word_ = *cycle.value;
|
2020-04-21 23:57:13 +00:00
|
|
|
|
set_mixer_levels(opll_detection_word_);
|
2020-04-04 00:05:36 +00:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-09-23 03:45:29 +00:00
|
|
|
|
break;
|
|
|
|
|
|
2018-09-23 04:07:46 +00:00
|
|
|
|
default:
|
2024-01-20 03:02:26 +00:00
|
|
|
|
logger.error().append("[output] Clearly some sort of typo");
|
2018-09-23 04:07:46 +00:00
|
|
|
|
break;
|
2018-09-23 03:45:29 +00:00
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
2020-04-04 00:05:36 +00:00
|
|
|
|
/*
|
|
|
|
|
TODO: implementation of the below is incomplete.
|
|
|
|
|
Re: io_port_control_
|
|
|
|
|
|
|
|
|
|
Set the TH pins for ports A and B as outputs. Set their output level
|
|
|
|
|
to any value desired by writing to bits 7 and 5. Read the state of both
|
|
|
|
|
TH pins back through bits 7 and 6 of port $DD. If the data returned is
|
|
|
|
|
the same as the data written, it's an export machine, otherwise it's
|
|
|
|
|
a domestic one.
|
|
|
|
|
|
|
|
|
|
— Charles MacDonald
|
|
|
|
|
*/
|
|
|
|
|
|
2018-09-23 03:45:29 +00:00
|
|
|
|
case CPU::Z80::PartialMachineCycle::Interrupt:
|
|
|
|
|
*cycle.value = 0xff;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default: break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-25 01:59:30 +00:00
|
|
|
|
// The pause button is debounced and takes effect only one line before pixels
|
|
|
|
|
// begin; time_until_debounce_ keeps track of the time until then.
|
|
|
|
|
time_until_debounce_ -= cycle.length;
|
|
|
|
|
if(time_until_debounce_ <= HalfCycles(0)) {
|
|
|
|
|
z80_.set_non_maskable_interrupt_line(pause_is_pressed_);
|
2019-07-29 19:38:41 +00:00
|
|
|
|
time_until_debounce_ = vdp_->get_time_until_line(-1);
|
2018-10-25 01:59:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 02:13:07 +00:00
|
|
|
|
return HalfCycles(0);
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 03:57:51 +00:00
|
|
|
|
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
2018-09-24 01:55:07 +00:00
|
|
|
|
return joysticks_;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-25 01:59:30 +00:00
|
|
|
|
// MARK: - Keyboard (i.e. the pause and reset buttons).
|
2020-01-24 03:57:51 +00:00
|
|
|
|
Inputs::Keyboard &get_keyboard() final {
|
2018-10-25 01:59:30 +00:00
|
|
|
|
return keyboard_;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-03 03:08:54 +00:00
|
|
|
|
bool keyboard_did_change_key(Inputs::Keyboard *, Inputs::Keyboard::Key key, bool is_pressed) final {
|
2018-10-25 01:59:30 +00:00
|
|
|
|
if(key == Inputs::Keyboard::Key::Enter) {
|
|
|
|
|
pause_is_pressed_ = is_pressed;
|
2020-03-03 03:08:54 +00:00
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(key == Inputs::Keyboard::Key::Escape) {
|
2018-10-25 01:59:30 +00:00
|
|
|
|
reset_is_pressed_ = is_pressed;
|
2020-03-03 03:08:54 +00:00
|
|
|
|
return true;
|
2018-10-25 01:59:30 +00:00
|
|
|
|
}
|
2020-03-03 03:08:54 +00:00
|
|
|
|
|
|
|
|
|
return false;
|
2018-10-25 01:59:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 03:57:51 +00:00
|
|
|
|
void reset_all_keys(Inputs::Keyboard *) final {
|
2018-10-25 01:59:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-10-24 02:30:24 +00:00
|
|
|
|
// MARK: - Configuration options.
|
2020-03-17 03:25:05 +00:00
|
|
|
|
std::unique_ptr<Reflection::Struct> get_options() final {
|
2020-03-18 22:26:22 +00:00
|
|
|
|
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
|
|
|
|
options->output = get_video_signal_configurable();
|
|
|
|
|
return options;
|
2018-10-24 02:30:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-18 22:26:22 +00:00
|
|
|
|
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
|
|
|
|
|
const auto options = dynamic_cast<Options *>(str.get());
|
|
|
|
|
set_video_signal_configurable(options->output);
|
2018-10-24 02:30:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 02:13:07 +00:00
|
|
|
|
private:
|
2023-01-01 02:54:14 +00:00
|
|
|
|
static constexpr TI::TMS::Personality tms_personality() {
|
2018-11-15 03:25:19 +00:00
|
|
|
|
switch(model) {
|
2019-01-26 00:31:44 +00:00
|
|
|
|
default:
|
2018-11-24 03:32:32 +00:00
|
|
|
|
case Target::Model::SG1000: return TI::TMS::TMS9918A;
|
2018-11-15 03:25:19 +00:00
|
|
|
|
case Target::Model::MasterSystem: return TI::TMS::SMSVDP;
|
2018-11-27 03:40:01 +00:00
|
|
|
|
case Target::Model::MasterSystem2: return TI::TMS::SMS2VDP;
|
2018-11-15 03:25:19 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-11 23:56:32 +00:00
|
|
|
|
inline uint8_t get_th_values() {
|
|
|
|
|
// Quick not on TH inputs here: if either is setup as an output, then the
|
|
|
|
|
// currently output level is returned. Otherwise they're fixed at 1.
|
|
|
|
|
return
|
2020-05-10 03:00:39 +00:00
|
|
|
|
uint8_t(
|
2018-10-11 23:56:32 +00:00
|
|
|
|
((io_port_control_ & 0x02) << 5) | ((io_port_control_&0x20) << 1) |
|
|
|
|
|
((io_port_control_ & 0x08) << 4) | (io_port_control_&0x80)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 02:13:07 +00:00
|
|
|
|
inline void update_audio() {
|
2020-04-12 16:46:40 +00:00
|
|
|
|
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(audio_divider)));
|
2018-09-22 02:13:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-21 23:57:13 +00:00
|
|
|
|
void set_mixer_levels(uint8_t mode) {
|
|
|
|
|
// This is as per the audio control register;
|
|
|
|
|
// see https://www.smspower.org/Development/AudioControlPort
|
|
|
|
|
update_audio();
|
2022-07-14 20:39:26 +00:00
|
|
|
|
audio_queue_.enqueue([this, mode] {
|
2020-04-21 23:57:13 +00:00
|
|
|
|
switch(mode & 3) {
|
|
|
|
|
case 0: // SN76489 only; the default.
|
|
|
|
|
mixer_.set_relative_volumes({1.0f, 0.0f});
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 1: // FM only.
|
|
|
|
|
mixer_.set_relative_volumes({0.0f, 1.0f});
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 2: // No audio.
|
|
|
|
|
mixer_.set_relative_volumes({0.0f, 0.0f});
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 3: // Both FM and SN76489.
|
|
|
|
|
mixer_.set_relative_volumes({0.5f, 0.5f});
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-20 01:37:05 +00:00
|
|
|
|
using Target = Analyser::Static::Sega::Target;
|
2020-05-12 04:11:46 +00:00
|
|
|
|
const Target::Region region_;
|
|
|
|
|
const Target::PagingScheme paging_scheme_;
|
2018-09-22 02:13:07 +00:00
|
|
|
|
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
2023-01-01 02:54:14 +00:00
|
|
|
|
JustInTimeActor<TI::TMS::TMS9918<tms_personality()>> vdp_;
|
2018-09-22 02:13:07 +00:00
|
|
|
|
|
2022-07-16 18:41:04 +00:00
|
|
|
|
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
2018-09-22 02:13:07 +00:00
|
|
|
|
TI::SN76489 sn76489_;
|
2020-04-08 03:15:26 +00:00
|
|
|
|
Yamaha::OPL::OPLL opll_;
|
|
|
|
|
Outputs::Speaker::CompoundSource<decltype(sn76489_), decltype(opll_)> mixer_;
|
2021-11-21 20:37:29 +00:00
|
|
|
|
Outputs::Speaker::PullLowpass<decltype(mixer_)> speaker_;
|
2020-04-04 00:05:36 +00:00
|
|
|
|
uint8_t opll_detection_word_ = 0xff;
|
2018-09-22 02:13:07 +00:00
|
|
|
|
|
2018-09-24 01:55:07 +00:00
|
|
|
|
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
2018-10-25 01:59:30 +00:00
|
|
|
|
Inputs::Keyboard keyboard_;
|
|
|
|
|
bool reset_is_pressed_ = false, pause_is_pressed_ = false;
|
2018-09-24 01:55:07 +00:00
|
|
|
|
|
2018-09-22 02:13:07 +00:00
|
|
|
|
HalfCycles time_since_sn76489_update_;
|
2018-10-25 01:59:30 +00:00
|
|
|
|
HalfCycles time_until_debounce_;
|
2018-09-23 03:45:29 +00:00
|
|
|
|
|
|
|
|
|
uint8_t ram_[8*1024];
|
|
|
|
|
uint8_t bios_[8*1024];
|
2018-09-23 21:36:30 +00:00
|
|
|
|
std::vector<uint8_t> cartridge_;
|
|
|
|
|
|
2018-10-11 23:56:32 +00:00
|
|
|
|
uint8_t io_port_control_ = 0x0f;
|
|
|
|
|
|
2020-04-04 00:05:36 +00:00
|
|
|
|
// This is a static constexpr for now; I may use it in the future.
|
|
|
|
|
static constexpr bool has_fm_audio_ = true;
|
|
|
|
|
|
2018-09-23 21:36:30 +00:00
|
|
|
|
// The memory map has a 1kb granularity; this is determined by the SG1000's 1kb of RAM.
|
|
|
|
|
const uint8_t *read_pointers_[64];
|
|
|
|
|
uint8_t *write_pointers_[64];
|
2018-09-25 01:34:42 +00:00
|
|
|
|
template <typename T> void map(T **target, uint8_t *source, size_t size, size_t start_address, size_t end_address = 0) {
|
|
|
|
|
if(!end_address) end_address = start_address + size;
|
|
|
|
|
for(auto address = start_address; address < end_address; address += 1024) {
|
|
|
|
|
target[address >> 10] = source ? &source[(address - start_address) & (size - 1)] : nullptr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint8_t paging_registers_[3] = {0, 1, 2};
|
2018-10-06 18:26:00 +00:00
|
|
|
|
uint8_t memory_control_ = 0;
|
2018-09-25 01:34:42 +00:00
|
|
|
|
void page_cartridge() {
|
2018-10-20 01:37:05 +00:00
|
|
|
|
// Either install the cartridge or don't; Japanese machines can't see
|
|
|
|
|
// anything but the cartridge.
|
|
|
|
|
if(!(memory_control_ & 0x40) || region_ == Target::Region::Japan) {
|
2018-10-06 18:26:00 +00:00
|
|
|
|
for(size_t c = 0; c < 3; ++c) {
|
|
|
|
|
const size_t start_addr = (paging_registers_[c] * 0x4000) % cartridge_.size();
|
|
|
|
|
map(
|
|
|
|
|
read_pointers_,
|
|
|
|
|
cartridge_.data() + start_addr,
|
2020-05-10 03:00:39 +00:00
|
|
|
|
std::min(size_t(0x4000), cartridge_.size() - start_addr),
|
2018-10-06 18:26:00 +00:00
|
|
|
|
c * 0x4000);
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-20 02:10:14 +00:00
|
|
|
|
// The first 1kb doesn't page though, if this is the Sega paging scheme.
|
|
|
|
|
if(paging_scheme_ == Target::PagingScheme::Sega) {
|
|
|
|
|
map(read_pointers_, cartridge_.data(), 0x400, 0x0000);
|
|
|
|
|
}
|
2018-10-06 18:26:00 +00:00
|
|
|
|
} else {
|
|
|
|
|
map(read_pointers_, nullptr, 0xc000, 0x0000);
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-18 23:13:15 +00:00
|
|
|
|
// Throw the BIOS on top if this machine has one and it isn't disabled.
|
2020-05-12 04:11:46 +00:00
|
|
|
|
if(has_bios_ && !(memory_control_ & 0x08)) {
|
2018-10-06 18:26:00 +00:00
|
|
|
|
map(read_pointers_, bios_, 8*1024, 0);
|
2018-09-23 21:36:30 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-12 04:11:46 +00:00
|
|
|
|
bool has_bios_ = true;
|
2018-09-22 02:13:07 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using namespace Sega::MasterSystem;
|
|
|
|
|
|
2024-01-13 03:03:19 +00:00
|
|
|
|
std::unique_ptr<Machine> Machine::MasterSystem(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
2018-09-23 20:34:47 +00:00
|
|
|
|
using Target = Analyser::Static::Sega::Target;
|
|
|
|
|
const Target *const sega_target = dynamic_cast<const Target *>(target);
|
2023-01-01 02:54:14 +00:00
|
|
|
|
|
|
|
|
|
switch(sega_target->model) {
|
2024-01-13 03:03:19 +00:00
|
|
|
|
case Target::Model::SG1000: return std::make_unique<ConcreteMachine<Target::Model::SG1000>>(*sega_target, rom_fetcher);
|
|
|
|
|
case Target::Model::MasterSystem: return std::make_unique<ConcreteMachine<Target::Model::MasterSystem>>(*sega_target, rom_fetcher);
|
|
|
|
|
case Target::Model::MasterSystem2: return std::make_unique<ConcreteMachine<Target::Model::MasterSystem2>>(*sega_target, rom_fetcher);
|
2023-01-01 02:54:14 +00:00
|
|
|
|
default:
|
|
|
|
|
assert(false);
|
2023-01-13 13:05:12 +00:00
|
|
|
|
return nullptr;
|
2023-01-01 02:54:14 +00:00
|
|
|
|
}
|
2018-09-22 02:13:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Machine::~Machine() {}
|