diff --git a/Analyser/Static/Sega/StaticAnalyser.cpp b/Analyser/Static/Sega/StaticAnalyser.cpp index c8d733c27..42fea5b2f 100644 --- a/Analyser/Static/Sega/StaticAnalyser.cpp +++ b/Analyser/Static/Sega/StaticAnalyser.cpp @@ -11,6 +11,9 @@ #include "Target.hpp" Analyser::Static::TargetList Analyser::Static::Sega::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { + if(media.cartridges.empty()) + return {}; + TargetList targets; std::unique_ptr target(new Target); @@ -23,10 +26,53 @@ Analyser::Static::TargetList Analyser::Static::Sega::GetTargets(const Media &med target->model = Target::Model::MasterSystem; } + // If this is a Master System title, look for a ROM header. + if(target->model == Target::Model::MasterSystem) { + const auto &data = media.cartridges.front()->get_segments()[0].data; + + // First try to locate a header. + size_t header_offset = 0; + size_t potential_offsets[] = {0x1ff0, 0x3ff0, 0x7ff0}; + for(auto potential_offset: potential_offsets) { + if(!memcmp(&data[potential_offset], "TMR SEGA", 8)) { + header_offset = potential_offset; + break; + } + } + + // If a header was found, use it to crib region. + if(header_offset) { + // Treat export titles as European by default; decline to + // do so only if (US) or (NTSC) is in the file name. + const uint8_t region = data[header_offset + 0x0f] >> 4; + switch(region) { + default: break; + case 4: { + std::string lowercase_name = file_name; + std::transform(lowercase_name.begin(), lowercase_name.end(), lowercase_name.begin(), ::tolower); + if(lowercase_name.find("(jp)") == std::string::npos) { + target->region = + (lowercase_name.find("(us)") == std::string::npos && + lowercase_name.find("(ntsc)") == std::string::npos) ? Target::Region::Europe : Target::Region::USA; + } + } break; + } + + // Also check for a Codemasters header. + // If one is found, set the paging scheme appropriately. + const uint16_t inverse_checksum = uint16_t(0x10000 - (data[0x7fe6] | (data[0x7fe7] << 8))); + if( + data[0x7fe3] >= 0x87 && data[0x7fe3] < 0x96 && // i.e. game is dated between 1987 and 1996 + (inverse_checksum&0xff) == data[0x7fe8] && + (inverse_checksum >> 8) == data[0x7fe9] && // i.e. the standard checksum appears to be present + !data[0x7fea] && !data[0x7feb] && !data[0x7fec] && !data[0x7fed] && !data[0x7fee] && !data[0x7fef] + ) { + target->paging_scheme = Target::PagingScheme::Codemasters; + } + } + } + target->media.cartridges = media.cartridges; - - if(!target->media.empty()) - targets.push_back(std::move(target)); - + targets.push_back(std::move(target)); return targets; } diff --git a/Analyser/Static/Sega/Target.hpp b/Analyser/Static/Sega/Target.hpp index 391b0e159..dcab665a1 100644 --- a/Analyser/Static/Sega/Target.hpp +++ b/Analyser/Static/Sega/Target.hpp @@ -19,7 +19,20 @@ struct Target: public ::Analyser::Static::Target { SG1000 }; + enum class Region { + Japan, + USA, + Europe + }; + + enum class PagingScheme { + Sega, + Codemasters + }; + Model model = Model::MasterSystem; + Region region = Region::Japan; + PagingScheme paging_scheme = PagingScheme::Sega; }; } diff --git a/Components/9918/9918.cpp b/Components/9918/9918.cpp index e4ae897a5..2830be0c6 100644 --- a/Components/9918/9918.cpp +++ b/Components/9918/9918.cpp @@ -21,6 +21,11 @@ const uint8_t StatusSpriteOverflow = 0x40; const int StatusSpriteCollisionShift = 5; const uint8_t StatusSpriteCollision = 0x20; +// 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole +// line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368. +const unsigned int CRTCyclesPerLine = 1365; +const unsigned int CRTCyclesDivider = 4; + struct ReverseTable { std::uint8_t map[256]; @@ -44,9 +49,7 @@ struct ReverseTable { Base::Base(Personality p) : personality_(p), - // 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole - // line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368. - crt_(new Outputs::CRT::CRT(1365, 4, Outputs::CRT::DisplayType::NTSC60, 4)) { + crt_(new Outputs::CRT::CRT(CRTCyclesPerLine, CRTCyclesDivider, Outputs::CRT::DisplayType::NTSC60, 4)) { switch(p) { case TI::TMS::TMS9918A: @@ -98,6 +101,21 @@ TMS9918::TMS9918(Personality p): crt_->set_immediate_default_phase(0.85f); } +void TMS9918::set_tv_standard(TVStandard standard) { + switch(standard) { + case TVStandard::PAL: + mode_timing_.total_lines = 313; + mode_timing_.first_vsync_line = 253; + crt_->set_new_display_type(CRTCyclesPerLine, Outputs::CRT::DisplayType::PAL50); + break; + default: + mode_timing_.total_lines = 262; + mode_timing_.first_vsync_line = 227; + crt_->set_new_display_type(CRTCyclesPerLine, Outputs::CRT::DisplayType::NTSC60); + break; + } +} + Outputs::CRT::CRT *TMS9918::get_crt() { return crt_.get(); } diff --git a/Components/9918/9918.hpp b/Components/9918/9918.hpp index 5dc552e67..efedee23f 100644 --- a/Components/9918/9918.hpp +++ b/Components/9918/9918.hpp @@ -66,8 +66,10 @@ class TMS9918: public Base { /*! Gets the current scan line; provided by the Master System only. */ uint8_t get_current_line(); + /*! Gets the current latched horizontal counter; provided by the Master System only. */ uint8_t get_latched_horizontal_counter(); + /*! Latches the current horizontal counter. */ void latch_horizontal_counter(); /*! diff --git a/Machines/MasterSystem/MasterSystem.cpp b/Machines/MasterSystem/MasterSystem.cpp index e2cb42463..944ac366b 100644 --- a/Machines/MasterSystem/MasterSystem.cpp +++ b/Machines/MasterSystem/MasterSystem.cpp @@ -80,14 +80,18 @@ class ConcreteMachine: public: ConcreteMachine(const Analyser::Static::Sega::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : model_(target.model), + region_(target.region), + paging_scheme_(target.paging_scheme), z80_(*this), sn76489_( - (target.model == Analyser::Static::Sega::Target::Model::SG1000) ? TI::SN76489::Personality::SN76489 : TI::SN76489::Personality::SMS, + (target.model == Target::Model::SG1000) ? TI::SN76489::Personality::SN76489 : TI::SN76489::Personality::SMS, audio_queue_, sn76489_divider), speaker_(sn76489_) { - speaker_.set_input_rate(3579545.0f / static_cast(sn76489_divider)); - set_clock_rate(3579545); + // Pick the clock rate based on the region. + const double clock_rate = target.region == Target::Region::Europe ? 3546893.0 : 3579540.0; + speaker_.set_input_rate(static_cast(clock_rate / sn76489_divider)); + set_clock_rate(clock_rate); // Instantiate the joysticks. joysticks_.emplace_back(new Joystick); @@ -104,10 +108,17 @@ class ConcreteMachine: cartridge_.resize(48*1024); memset(&cartridge_[48*1024 - new_space], 0xff, new_space); } + + 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; + } page_cartridge(); - // Establish the BIOS (if relevant) and RAM. - if(target.model == Analyser::Static::Sega::Target::Model::MasterSystem) { + // Load the BIOS if relevant. + if(has_bios()) { const auto roms = rom_fetcher("MasterSystem", {"bios.sms"}); if(!roms[0]) { throw ROMMachine::Error::MissingROMs; @@ -115,8 +126,10 @@ class ConcreteMachine: roms[0]->resize(8*1024); memcpy(&bios_, roms[0]->data(), roms[0]->size()); - map(read_pointers_, bios_, 8*1024, 0); + } + // Map RAM. + if(model_ == Target::Model::MasterSystem) { map(read_pointers_, ram_, 8*1024, 0xc000, 0x10000); map(write_pointers_, ram_, 8*1024, 0xc000, 0x10000); } else { @@ -124,6 +137,7 @@ class ConcreteMachine: map(write_pointers_, ram_, 1024, 0xc000, 0x10000); } + // Apple a relatively low low-pass filter. More guidance needed here. speaker_.set_high_frequency_cutoff(8000); } @@ -132,7 +146,10 @@ class ConcreteMachine: } void setup_output(float aspect_ratio) override { - vdp_.reset(new TI::TMS::TMS9918(model_ == Analyser::Static::Sega::Target::Model::SG1000 ? TI::TMS::TMS9918A : TI::TMS::SMSVDP)); + vdp_.reset(new TI::TMS::TMS9918(model_ == Target::Model::SG1000 ? TI::TMS::TMS9918A : TI::TMS::SMSVDP)); + vdp_->set_tv_standard( + (region_ == Target::Region::Europe) ? + TI::TMS::TMS9918::TVStandard::PAL : TI::TMS::TMS9918::TVStandard::NTSC); get_crt()->set_video_signal(Outputs::CRT::VideoSignal::Composite); } @@ -165,10 +182,20 @@ class ConcreteMachine: break; case CPU::Z80::PartialMachineCycle::Write: - if(address >= 0xfffd && cartridge_.size() > 48*1024) { - if(paging_registers_[address - 0xfffd] != *cycle.value) { - paging_registers_[address - 0xfffd] = *cycle.value; - page_cartridge(); + 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(); + } } } @@ -222,7 +249,7 @@ class ConcreteMachine: case CPU::Z80::PartialMachineCycle::Output: switch(address & 0xc1) { case 0x00: - if(model_ == Analyser::Static::Sega::Target::Model::MasterSystem) { + if(model_ == Target::Model::MasterSystem) { // TODO: Obey the RAM enable. memory_control_ = *cycle.value; page_cartridge(); @@ -253,7 +280,7 @@ class ConcreteMachine: time_until_interrupt_ = vdp_->get_time_until_interrupt(); break; case 0xc0: - LOG("TODO: [output] I/O port A/N; " << *cycle.value); + LOG("TODO: [output] I/O port A/N; " << int(*cycle.value)); break; case 0xc1: LOG("TODO: [output] I/O port B/misc"); @@ -312,7 +339,10 @@ class ConcreteMachine: vdp_->run_for(time_since_vdp_update_.flush()); } - Analyser::Static::Sega::Target::Model model_; + using Target = Analyser::Static::Sega::Target; + Target::Model model_; + Target::Region region_; + Target::PagingScheme paging_scheme_; CPU::Z80::Processor z80_; std::unique_ptr vdp_; @@ -345,8 +375,9 @@ class ConcreteMachine: uint8_t paging_registers_[3] = {0, 1, 2}; uint8_t memory_control_ = 0; void page_cartridge() { - // Either install the cartridge or don't. - if(!(memory_control_ & 0x40)) { + // Either install the cartridge or don't; Japanese machines can't see + // anything but the cartridge. + if(!(memory_control_ & 0x40) || region_ == Target::Region::Japan) { for(size_t c = 0; c < 3; ++c) { const size_t start_addr = (paging_registers_[c] * 0x4000) % cartridge_.size(); map( @@ -356,17 +387,22 @@ class ConcreteMachine: c * 0x4000); } - // The first 1kb doesn't page though. - map(read_pointers_, cartridge_.data(), 0x400, 0x0000); + // 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); + } } else { map(read_pointers_, nullptr, 0xc000, 0x0000); } // Throw the BIOS on top if this machine has one and it isn't disabled. - if(model_ == Analyser::Static::Sega::Target::Model::MasterSystem && !(memory_control_ & 0x08)) { + if(has_bios() && !(memory_control_ & 0x08)) { map(read_pointers_, bios_, 8*1024, 0); } } + bool has_bios() { + return model_ == Target::Model::MasterSystem && region_ != Target::Region::Japan; + } }; }