diff --git a/Analyser/Static/ZXSpectrum/Target.hpp b/Analyser/Static/ZXSpectrum/Target.hpp index e2711ec55..76eada20e 100644 --- a/Analyser/Static/ZXSpectrum/Target.hpp +++ b/Analyser/Static/ZXSpectrum/Target.hpp @@ -19,11 +19,15 @@ namespace ZXSpectrum { struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(Model, + SixteenK, + FortyEightK, + OneTwoEightK, + Plus2, Plus2a, Plus3, ); - Model model = Model::Plus2a; + Model model = Model::Plus2; bool should_hold_enter = false; Target(): Analyser::Static::Target(Machine::ZXSpectrum) { diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp index 371697511..0da1e59c2 100644 --- a/Machines/Sinclair/ZXSpectrum/Video.hpp +++ b/Machines/Sinclair/ZXSpectrum/Video.hpp @@ -18,7 +18,9 @@ namespace Sinclair { namespace ZXSpectrum { enum class VideoTiming { - Plus3 + FortyEightK, + OneTwoEightK, + Plus3, }; /* @@ -67,48 +69,103 @@ template class Video { }; static constexpr Timings get_timings() { - // Amstrad gate array timings, classic statement: - // - // Contention begins 14361 cycles "after interrupt" and follows the pattern [1, 0, 7, 6 5 4, 3, 2]. - // The first four bytes of video are fetched at 14365–14368 cycles, in the order [pixels, attribute, pixels, attribute]. - // - // For my purposes: - // - // Video fetching always begins at 0. Since there are 311*228 = 70908 cycles per frame, and the interrupt - // should "occur" (I assume: begin) 14365 before that, it should actually begin at 70908 - 14365 = 56543. - // - // Contention begins four cycles before the first video fetch, so it begins at 70904. I don't currently - // know whether the four cycles is true across all models, so it's given here as convention_leadin. - // - // ... except that empirically that all seems to be two cycles off. So maybe I misunderstand what the - // contention patterns are supposed to indicate relative to MREQ? It's frustrating that all documentation - // I can find is vaguely in terms of contention patterns, and what they mean isn't well-defined in terms - // of regular Z80 signalling. - constexpr Timings result = { - .cycles_per_line = 228 * 2, - .lines_per_frame = 311, + if constexpr (timing == VideoTiming::Plus3) { + // Amstrad gate array timings, classic statement: + // + // Contention begins 14361 cycles "after interrupt" and follows the pattern [1, 0, 7, 6 5 4, 3, 2]. + // The first four bytes of video are fetched at 14365–14368 cycles, in the order [pixels, attribute, pixels, attribute]. + // + // For my purposes: + // + // Video fetching always begins at 0. Since there are 311*228 = 70908 cycles per frame, and the interrupt + // should "occur" (I assume: begin) 14365 before that, it should actually begin at 70908 - 14365 = 56543. + // + // Contention begins four cycles before the first video fetch, so it begins at 70904. I don't currently + // know whether the four cycles is true across all models, so it's given here as convention_leadin. + // + // ... except that empirically that all seems to be two cycles off. So maybe I misunderstand what the + // contention patterns are supposed to indicate relative to MREQ? It's frustrating that all documentation + // I can find is vaguely in terms of contention patterns, and what they mean isn't well-defined in terms + // of regular Z80 signalling. + constexpr Timings result = { + .cycles_per_line = 228 * 2, + .lines_per_frame = 311, - // i.e. video fetching begins five cycles after the start of the - // contended memory pattern below; that should put a clear two - // cycles between a Z80 access and the first video fetch. - .contention_leadin = 5 * 2, - .contention_duration = 129 * 2, + // i.e. video fetching begins five cycles after the start of the + // contended memory pattern below; that should put a clear two + // cycles between a Z80 access and the first video fetch. + .contention_leadin = 5 * 2, + .contention_duration = 129 * 2, - // i.e. interrupt is first signalled 14368 cycles before the first video fetch. - .interrupt_time = (228*311 - 14368) * 2, + // i.e. interrupt is first signalled 14368 cycles before the first video fetch. + .interrupt_time = (228*311 - 14368) * 2, - .delays = { - 2, 1, - 0, 0, - 14, 13, - 12, 11, - 10, 9, - 8, 7, - 6, 5, - 4, 3, - } - }; - return result; + .delays = { + 2, 1, + 0, 0, + 14, 13, + 12, 11, + 10, 9, + 8, 7, + 6, 5, + 4, 3, + } + }; + + return result; + } + + // TODO: fix 48kb and 128kb timings, below. + + if constexpr (timing == VideoTiming::OneTwoEightK) { + constexpr Timings result = { + .cycles_per_line = 228 * 2, + .lines_per_frame = 311, + + .contention_leadin = 2 * 2, + .contention_duration = 128 * 2, + + .interrupt_time = (228*311 - 14366) * 2, + + .delays = { + 12, 11, + 10, 9, + 8, 7, + 6, 5, + 4, 3, + 2, 1, + 0, 0, + 0, 0, + } + }; + + return result; + } + + if constexpr (timing == VideoTiming::FortyEightK) { + constexpr Timings result = { + .cycles_per_line = 224 * 2, + .lines_per_frame = 312, + + .contention_leadin = 2 * 2, + .contention_duration = 128 * 2, + + .interrupt_time = (224*312 - 14339) * 2, + + .delays = { + 12, 11, + 10, 9, + 8, 7, + 6, 5, + 4, 3, + 2, 1, + 0, 0, + 0, 0, + } + }; + + return result; + } } // TODO: how long is the interrupt line held for? @@ -236,9 +293,14 @@ template class Video { if(offset >= burst_position && offset < burst_position+burst_length && end_offset > offset) { const int burst_duration = std::min(burst_position + burst_length, end_offset) - offset; - crt_.output_colour_burst(burst_duration, 116, is_alternate_line_); + + if constexpr (timing >= VideoTiming::OneTwoEightK) { + crt_.output_colour_burst(burst_duration, 116, is_alternate_line_); + // The colour burst phase above is an empirical guess. I need to research further. + } else { + crt_.output_default_colour_burst(burst_duration); + } offset += burst_duration; - // The colour burst phase above is an empirical guess. I need to research further. } if(offset >= burst_position+burst_length && end_offset > offset) { @@ -259,9 +321,21 @@ template class Video { crt_.output_level(duration); } + static constexpr int half_cycles_per_line() { + if constexpr (timing == VideoTiming::FortyEightK) { + // TODO: determine real figure here, if one exists. + // The source I'm looking at now suggests that the theoretical + // ideal of 224*2 ignores the real-life effects of separate + // crystals, so I've nudged this experimentally. + return 224*2 - 1; + } else { + return 227*2; + } + } + public: Video() : - crt_(227 * 2, 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2) + crt_(half_cycles_per_line(), 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2) { // Show only the centre 80% of the TV frame. crt_.set_display_type(Outputs::Display::DisplayType::RGB); @@ -327,20 +401,24 @@ template class Video { */ uint8_t get_floating_value() const { constexpr auto timings = get_timings(); + const uint8_t out_of_bounds = (timing == VideoTiming::Plus3) ? last_contended_access_ : 0xff; + const int line = time_into_frame_ / timings.cycles_per_line; - if(line >= 192) return 0xff; + if(line >= 192) { + return out_of_bounds; + } const int time_into_line = time_into_frame_ % timings.cycles_per_line; if(time_into_line >= 256 || (time_into_line&8)) { - return last_contended_access_; + return out_of_bounds; } // The +2a and +3 always return the low bit as set. + const uint8_t value = last_fetches_[(time_into_line >> 1) & 3]; if constexpr (timing == VideoTiming::Plus3) { - return last_fetches_[(time_into_line >> 1) & 3] | 1; + return value | 1; } - - return last_fetches_[(time_into_line >> 1) & 3]; + return value; } /*! diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp index 717026ee4..165099989 100644 --- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -81,8 +81,29 @@ template class ConcreteMachine: // With only the +2a and +3 currently supported, the +3 ROM is always // the one required. - const auto roms = - rom_fetcher({ {"ZXSpectrum", "the +2a/+3 ROM", "plus3.rom", 64 * 1024, 0x96e3c17a} }); + std::vector rom_names; + const std::string machine = "ZXSpectrum"; + switch(model) { + case Model::SixteenK: + case Model::FortyEightK: + rom_names.emplace_back(machine, "the 48kb ROM", "48.rom", 16 * 1024, 0xddee531f); + break; + + case Model::OneTwoEightK: + rom_names.emplace_back(machine, "the 128kb ROM", "128.rom", 32 * 1024, 0x2cbe8995); + break; + + case Model::Plus2: + rom_names.emplace_back(machine, "the +2 ROM", "plus2.rom", 32 * 1024, 0xe7a517dc); + break; + + case Model::Plus2a: + case Model::Plus3: { + const std::initializer_list crc32s = { 0x96e3c17a, 0xbe0d9ec4 }; + rom_names.emplace_back(machine, "the +2a/+3 ROM", "plus3.rom", 64 * 1024, crc32s); + } break; + } + const auto roms = rom_fetcher(rom_names); if(!roms[0]) throw ROMMachine::Error::MissingROMs; memcpy(rom_.data(), roms[0]->data(), std::min(rom_.size(), roms[0]->size())); @@ -110,7 +131,7 @@ template class ConcreteMachine: } static constexpr unsigned int clock_rate() { -// constexpr unsigned int ClockRate = 3'500'000; + constexpr unsigned int OriginalClockRate = 3'500'000; constexpr unsigned int Plus3ClockRate = 3'546'875; // See notes below; this is a guess. // Notes on timing for the +2a and +3: @@ -137,7 +158,7 @@ template class ConcreteMachine: // the Spectrum is a PAL machine with a fixed colour phase relationship. For // this emulator's world, that's a first! - return Plus3ClockRate; + return model < Model::OneTwoEightK ? OriginalClockRate : Plus3ClockRate; } // MARK: - TimedMachine. @@ -189,18 +210,90 @@ template class ConcreteMachine: const uint16_t address = cycle.address ? *cycle.address : 0x0000; // Apply contention if necessary. - if( - is_contended_[address >> 14] && - cycle.operation >= PartialMachineCycle::ReadOpcodeStart && - cycle.operation <= PartialMachineCycle::WriteStart) { - // Assumption here: the trigger for the ULA inserting a delay is the falling edge + if constexpr (model >= Model::Plus2a) { + // Model applied: the trigger for the ULA inserting a delay is the falling edge // of MREQ, which is always half a cycle into a read or write. - // - // TODO: somehow provide that information in the PartialMachineCycle? + if( + is_contended_[address >> 14] && + cycle.operation >= PartialMachineCycle::ReadOpcodeStart && + cycle.operation <= PartialMachineCycle::WriteStart) { - const HalfCycles delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1)); - advance(cycle.length + delay); - return delay; + const HalfCycles delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1)); + advance(cycle.length + delay); + return delay; + } + } else { + switch(cycle.operation) { + default: + advance(cycle.length); + return HalfCycles(0); + + case CPU::Z80::PartialMachineCycle::InputStart: + case CPU::Z80::PartialMachineCycle::OutputStart: { + // The port address is loaded prior to IOREQ being visible; a contention + // always occurs if it is in the $4000–$8000 range regardless of current + // memory mapping. + HalfCycles delay; + HalfCycles time = video_.time_since_flush() + HalfCycles(1); + + if((address & 0xc000) == 0x4000) { + for(int c = 0; c < ((address & 1) ? 4 : 2); c++) { + const auto next_delay = video_.last_valid()->access_delay(time); + delay += next_delay; + time += next_delay + 2; + } + } else { + if(!(address & 1)) { + delay = video_.last_valid()->access_delay(time + HalfCycles(2)); + } + } + + advance(cycle.length + delay); + return delay; + } + + case PartialMachineCycle::ReadOpcodeStart: + case PartialMachineCycle::ReadStart: + case PartialMachineCycle::WriteStart: { + // These all start by loading the address bus, then set MREQ + // half a cycle later. + if(is_contended_[address >> 14]) { + const HalfCycles delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1)); + + advance(cycle.length + delay); + return delay; + } + } + + case PartialMachineCycle::Internal: { + // Whatever's on the address bus will remain there, without IOREQ or + // MREQ interceding, for this entire bus cycle. So apply contentions + // all the way along. + if(is_contended_[address >> 14]) { + const auto half_cycles = cycle.length.as(); + assert(!(half_cycles & 1)); + + HalfCycles time = video_.time_since_flush() + HalfCycles(1); + HalfCycles delay; + for(int c = 0; c < half_cycles; c += 2) { + const auto next_delay = video_.last_valid()->access_delay(time); + delay += next_delay; + time += next_delay + 2; + } + + advance(cycle.length + delay); + return delay; + } + } + + case CPU::Z80::PartialMachineCycle::Input: + case CPU::Z80::PartialMachineCycle::Output: + case CPU::Z80::PartialMachineCycle::Read: + case CPU::Z80::PartialMachineCycle::Write: + case CPU::Z80::PartialMachineCycle::ReadOpcode: + // For these, carry on into the actual handler, below. + break; + } } // For all other machine cycles, model the action as happening at the end of the machine cycle; @@ -214,7 +307,7 @@ template class ConcreteMachine: // Fast loading: ROM version. // // The below patches over part of the 'LD-BYTES' routine from the 48kb ROM. - if(use_fast_tape_hack_ && address == 0x056b && read_pointers_[0] == &rom_[0xc000]) { + if(use_fast_tape_hack_ && address == 0x056b && read_pointers_[0] == &rom_[classic_rom_offset()]) { // Stop pressing enter, if neccessry. if(duration_to_press_enter_ > Cycles(0)) { duration_to_press_enter_ = Cycles(0); @@ -228,10 +321,21 @@ template class ConcreteMachine: } case PartialMachineCycle::Read: + if constexpr (model == Model::SixteenK) { + // Assumption: with nothing mapped above 0x8000 on the 16kb Spectrum, + // read the floating bus. + if(address >= 0x8000) { + *cycle.value = video_->get_floating_value(); + break; + } + } + *cycle.value = read_pointers_[address >> 14][address]; - if(is_contended_[address >> 14]) { - video_->set_last_contended_area_access(*cycle.value); + if constexpr (model >= Model::Plus2a) { + if(is_contended_[address >> 14]) { + video_->set_last_contended_area_access(*cycle.value); + } } break; @@ -243,9 +347,11 @@ template class ConcreteMachine: write_pointers_[address >> 14][address] = *cycle.value; - // Fill the floating bus buffer if this write is within the contended area. - if(is_contended_[address >> 14]) { - video_->set_last_contended_area_access(*cycle.value); + if constexpr (model >= Model::Plus2a) { + // Fill the floating bus buffer if this write is within the contended area. + if(is_contended_[address >> 14]) { + video_->set_last_contended_area_access(*cycle.value); + } } break; @@ -263,41 +369,49 @@ template class ConcreteMachine: } // Test for classic 128kb paging register (i.e. port 7ffd). - if((address & 0xc002) == 0x4000) { - port7ffd_ = *cycle.value; - update_memory_map(); + if constexpr (model >= Model::OneTwoEightK) { + if((address & 0xc002) == 0x4000) { + port7ffd_ = *cycle.value; + update_memory_map(); - // Set the proper video base pointer. - set_video_address(); + // Set the proper video base pointer. + set_video_address(); - // Potentially lock paging, _after_ the current - // port values have taken effect. - disable_paging_ |= *cycle.value & 0x20; - } - - // Test for +2a/+3 paging (i.e. port 1ffd). - if((address & 0xf002) == 0x1000) { - port1ffd_ = *cycle.value; - update_memory_map(); - update_video_base(); - - if constexpr (model == Model::Plus3) { - fdc_->set_motor_on(*cycle.value & 0x08); + // Potentially lock paging, _after_ the current + // port values have taken effect. + disable_paging_ |= *cycle.value & 0x20; } } - if((address & 0xc002) == 0xc000) { - // Select AY register. - update_audio(); - GI::AY38910::Utility::select_register(ay_, *cycle.value); + // Test for +2a/+3 paging (i.e. port 1ffd). + if constexpr (model >= Model::Plus2a) { + if((address & 0xf002) == 0x1000) { + port1ffd_ = *cycle.value; + update_memory_map(); + update_video_base(); + + if constexpr (model == Model::Plus3) { + fdc_->set_motor_on(*cycle.value & 0x08); + } + } } - if((address & 0xc002) == 0x8000) { - // Write to AY register. - update_audio(); - GI::AY38910::Utility::write_data(ay_, *cycle.value); + // Route to the AY if one is fitted. + if constexpr (model >= Model::OneTwoEightK) { + if((address & 0xc002) == 0xc000) { + // Select AY register. + update_audio(); + GI::AY38910::Utility::select_register(ay_, *cycle.value); + } + + if((address & 0xc002) == 0x8000) { + // Write to AY register. + update_audio(); + GI::AY38910::Utility::write_data(ay_, *cycle.value); + } } + // Check for FDC accesses. if constexpr (model == Model::Plus3) { switch(address) { default: break; @@ -308,10 +422,13 @@ template class ConcreteMachine: } break; - case PartialMachineCycle::Input: + case PartialMachineCycle::Input: { + bool did_match = false; *cycle.value = 0xff; if(!(address&1)) { + did_match = true; + // Port FE: // // address b8+: mask of keyboard lines to select @@ -339,17 +456,23 @@ template class ConcreteMachine: } } - if((address & 0xc002) == 0xc000) { - // Read from AY register. - update_audio(); - *cycle.value &= GI::AY38910::Utility::read(ay_); + if constexpr (model >= Model::OneTwoEightK) { + if((address & 0xc002) == 0xc000) { + did_match = true; + + // Read from AY register. + update_audio(); + *cycle.value &= GI::AY38910::Utility::read(ay_); + } } - // Check for a floating bus read; these are particularly arcane - // on the +2a/+3. See footnote to https://spectrumforeveryone.com/technical/memory-contention-floating-bus/ - // and, much more rigorously, http://sky.relative-path.com/zx/floating_bus.html - if(!disable_paging_ && (address & 0xf003) == 0x0001) { - *cycle.value &= video_->get_floating_value(); + if constexpr (model >= Model::Plus2a) { + // Check for a +2a/+3 floating bus read; these are particularly arcane. + // See footnote to https://spectrumforeveryone.com/technical/memory-contention-floating-bus/ + // and, much more rigorously, http://sky.relative-path.com/zx/floating_bus.html + if(!disable_paging_ && (address & 0xf003) == 0x0001) { + *cycle.value &= video_->get_floating_value(); + } } if constexpr (model == Model::Plus3) { @@ -360,7 +483,13 @@ template class ConcreteMachine: break; } } - break; + + if constexpr (model < Model::Plus2) { + if(!did_match) { + *cycle.value = video_->get_floating_value(); + } + } + } break; } return HalfCycles(0); @@ -571,10 +700,14 @@ template class ConcreteMachine: } void set_memory(int bank, uint8_t source) { - is_contended_[bank] = (source >= 4 && source < 8); + if constexpr (model >= Model::Plus2a) { + is_contended_[bank] = (source >= 4 && source < 8); + } else { + is_contended_[bank] = source & 1; + } pages_[bank] = source; - uint8_t *read = (source < 0x80) ? &ram_[source * 16384] : &rom_[(source & 0x7f) * 16384]; + uint8_t *const read = (source < 0x80) ? &ram_[source * 16384] : &rom_[(source & 0x7f) * 16384]; const auto offset = bank*16384; read_pointers_[bank] = read - offset; @@ -607,8 +740,15 @@ template class ConcreteMachine: } // MARK: - Video. - static constexpr VideoTiming video_timing = VideoTiming::Plus3; - JustInTimeActor> video_; + using VideoType = + std::conditional_t< + model <= Model::FortyEightK, Video, + std::conditional_t< + model <= Model::Plus2, Video, + Video + > + >; + JustInTimeActor video_; // MARK: - Keyboard. Sinclair::ZX::Keyboard::Keyboard keyboard_; @@ -695,6 +835,22 @@ template class ConcreteMachine: return true; } + static constexpr int classic_rom_offset() { + switch(model) { + case Model::SixteenK: + case Model::FortyEightK: + return 0x0000; + + case Model::OneTwoEightK: + case Model::Plus2: + return 0x4000; + + case Model::Plus2a: + case Model::Plus3: + return 0xc000; + } + } + // MARK: - Disc. JustInTimeActor fdc_; @@ -712,8 +868,12 @@ Machine *Machine::ZXSpectrum(const Analyser::Static::Target *target, const ROMMa const auto zx_target = dynamic_cast(target); switch(zx_target->model) { - case Model::Plus2a: return new ConcreteMachine(*zx_target, rom_fetcher); - case Model::Plus3: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::SixteenK: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::FortyEightK: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::OneTwoEightK: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::Plus2: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::Plus2a: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::Plus3: return new ConcreteMachine(*zx_target, rom_fetcher); } return nullptr; diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h index 1193281f4..62df779b9 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h @@ -63,6 +63,10 @@ typedef NS_ENUM(NSInteger, CSMachineOricDiskInterface) { }; typedef NS_ENUM(NSInteger, CSMachineSpectrumModel) { + CSMachineSpectrumModelSixteenK, + CSMachineSpectrumModelFortyEightK, + CSMachineSpectrumModelOneTwoEightK, + CSMachineSpectrumModelPlus2, CSMachineSpectrumModelPlus2a, CSMachineSpectrumModelPlus3, }; diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm index 993a5c047..8f4c693b4 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm @@ -194,8 +194,12 @@ using Target = Analyser::Static::ZXSpectrum::Target; auto target = std::make_unique(); switch(model) { - case CSMachineSpectrumModelPlus2a: target->model = Target::Model::Plus2a; break; - case CSMachineSpectrumModelPlus3: target->model = Target::Model::Plus3; break; + case CSMachineSpectrumModelSixteenK: target->model = Target::Model::SixteenK; break; + case CSMachineSpectrumModelFortyEightK: target->model = Target::Model::FortyEightK; break; + case CSMachineSpectrumModelOneTwoEightK: target->model = Target::Model::OneTwoEightK; break; + case CSMachineSpectrumModelPlus2: target->model = Target::Model::Plus2; break; + case CSMachineSpectrumModelPlus2a: target->model = Target::Model::Plus2a; break; + case CSMachineSpectrumModelPlus3: target->model = Target::Model::Plus3; break; } _targets.push_back(std::move(target)); } diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib index 31e1925f8..cc68c2170 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib +++ b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib @@ -18,7 +18,7 @@ - + @@ -584,11 +584,11 @@ Gw - + - + @@ -602,7 +602,7 @@ Gw - + @@ -622,24 +622,28 @@ Gw - + - - + + - + + + + + - + diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift index f358c8666..3445c27da 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift +++ b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift @@ -298,6 +298,10 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { case "spectrum": var model: CSMachineSpectrumModel = .plus2a switch spectrumModelTypeButton.selectedItem!.tag { + case 16: model = .sixteenK + case 48: model = .fortyEightK + case 128: model = .oneTwoEightK + case 2: model = .plus2 case 21: model = .plus2a case 3: model = .plus3 default: break diff --git a/OSBindings/Qt/mainwindow.cpp b/OSBindings/Qt/mainwindow.cpp index 630bff4a6..6836844f1 100644 --- a/OSBindings/Qt/mainwindow.cpp +++ b/OSBindings/Qt/mainwindow.cpp @@ -1258,9 +1258,13 @@ void MainWindow::start_spectrum() { using Target = Analyser::Static::ZXSpectrum::Target; auto target = std::make_unique(); - switch(ui->oricModelComboBox->currentIndex()) { - default: target->model = Target::Model::Plus2a; break; - case 1: target->model = Target::Model::Plus3; break; + switch(ui->spectrumModelComboBox->currentIndex()) { + default: target->model = Target::Model::SixteenK; break; + case 1: target->model = Target::Model::FortyEightK; break; + case 2: target->model = Target::Model::OneTwoEightK; break; + case 3: target->model = Target::Model::Plus2; break; + case 4: target->model = Target::Model::Plus2a; break; + case 5: target->model = Target::Model::Plus3; break; } launchTarget(std::move(target)); diff --git a/OSBindings/Qt/mainwindow.ui b/OSBindings/Qt/mainwindow.ui index 95a835436..4e576e4c3 100644 --- a/OSBindings/Qt/mainwindow.ui +++ b/OSBindings/Qt/mainwindow.ui @@ -551,6 +551,26 @@ + + + 16kb + + + + + 48kb + + + + + 128kb + + + + + +2 + + +2a diff --git a/ROMImages/ZXSpectrum/128.rom b/ROMImages/ZXSpectrum/128.rom new file mode 100644 index 000000000..1ddee8c96 Binary files /dev/null and b/ROMImages/ZXSpectrum/128.rom differ diff --git a/ROMImages/ZXSpectrum/48.rom b/ROMImages/ZXSpectrum/48.rom new file mode 100644 index 000000000..4d6895e0b Binary files /dev/null and b/ROMImages/ZXSpectrum/48.rom differ diff --git a/ROMImages/ZXSpectrum/plus2.rom b/ROMImages/ZXSpectrum/plus2.rom new file mode 100644 index 000000000..2bd54e18e Binary files /dev/null and b/ROMImages/ZXSpectrum/plus2.rom differ diff --git a/ROMImages/ZXSpectrum/readme.txt b/ROMImages/ZXSpectrum/readme.txt index 63a56a9eb..afcbe6bef 100644 --- a/ROMImages/ZXSpectrum/readme.txt +++ b/ROMImages/ZXSpectrum/readme.txt @@ -9,3 +9,6 @@ material but retain that copyright"." With that in mind, Amstrad have kindly given their permission for the redistribution of their copyrighted material but retain that copyright. Material expected here, copyright Amstrad: plus3.rom — the +2a/+3 ROM file, 64kb in size. +plus2.rom – the +2 ROM file, 32kb in size. +128.rom – the 128kb ROM file, 32kb in size. +48.rom – the 16/48kb ROM file, 16kb in size. \ No newline at end of file