1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-06 01:28:57 +00:00

Merge pull request #911 from TomHarte/48kbSpectrum

Adds the 48kb and 128kb Spectrums.
This commit is contained in:
Thomas Harte 2021-04-15 22:25:07 -04:00 committed by GitHub
commit 246fd9442f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 411 additions and 126 deletions

View File

@ -19,11 +19,15 @@ namespace ZXSpectrum {
struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> {
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) {

View File

@ -18,7 +18,9 @@ namespace Sinclair {
namespace ZXSpectrum {
enum class VideoTiming {
Plus3
FortyEightK,
OneTwoEightK,
Plus3,
};
/*
@ -67,48 +69,103 @@ template <VideoTiming timing> 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 1436514368 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 1436514368 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 <VideoTiming timing> 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 <VideoTiming timing> 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 <VideoTiming timing> 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;
}
/*!

View File

@ -81,8 +81,29 @@ template<Model model> 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<ROMMachine::ROM> 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<uint32_t> 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<Model model> 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<Model model> 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<Model model> 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<int>();
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<Model model> 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<Model model> 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<Model model> 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<Model model> 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<Model model> 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<Model model> 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<Model model> 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<Model model> 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<Model model> class ConcreteMachine:
}
// MARK: - Video.
static constexpr VideoTiming video_timing = VideoTiming::Plus3;
JustInTimeActor<Video<video_timing>> video_;
using VideoType =
std::conditional_t<
model <= Model::FortyEightK, Video<VideoTiming::FortyEightK>,
std::conditional_t<
model <= Model::Plus2, Video<VideoTiming::OneTwoEightK>,
Video<VideoTiming::Plus3>
>
>;
JustInTimeActor<VideoType> video_;
// MARK: - Keyboard.
Sinclair::ZX::Keyboard::Keyboard keyboard_;
@ -695,6 +835,22 @@ template<Model model> 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<Amstrad::FDC, Cycles> fdc_;
@ -712,8 +868,12 @@ Machine *Machine::ZXSpectrum(const Analyser::Static::Target *target, const ROMMa
const auto zx_target = dynamic_cast<const Analyser::Static::ZXSpectrum::Target *>(target);
switch(zx_target->model) {
case Model::Plus2a: return new ConcreteMachine<Model::Plus2a>(*zx_target, rom_fetcher);
case Model::Plus3: return new ConcreteMachine<Model::Plus3>(*zx_target, rom_fetcher);
case Model::SixteenK: return new ConcreteMachine<Model::SixteenK>(*zx_target, rom_fetcher);
case Model::FortyEightK: return new ConcreteMachine<Model::FortyEightK>(*zx_target, rom_fetcher);
case Model::OneTwoEightK: return new ConcreteMachine<Model::OneTwoEightK>(*zx_target, rom_fetcher);
case Model::Plus2: return new ConcreteMachine<Model::Plus2>(*zx_target, rom_fetcher);
case Model::Plus2a: return new ConcreteMachine<Model::Plus2a>(*zx_target, rom_fetcher);
case Model::Plus3: return new ConcreteMachine<Model::Plus3>(*zx_target, rom_fetcher);
}
return nullptr;

View File

@ -63,6 +63,10 @@ typedef NS_ENUM(NSInteger, CSMachineOricDiskInterface) {
};
typedef NS_ENUM(NSInteger, CSMachineSpectrumModel) {
CSMachineSpectrumModelSixteenK,
CSMachineSpectrumModelFortyEightK,
CSMachineSpectrumModelOneTwoEightK,
CSMachineSpectrumModelPlus2,
CSMachineSpectrumModelPlus2a,
CSMachineSpectrumModelPlus3,
};

View File

@ -194,8 +194,12 @@
using Target = Analyser::Static::ZXSpectrum::Target;
auto target = std::make_unique<Target>();
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));
}

View File

@ -18,7 +18,7 @@
<windowStyleMask key="styleMask" titled="YES" documentModal="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="590" height="316"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1440"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="590" height="316"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@ -584,11 +584,11 @@ Gw
</tabViewItem>
<tabViewItem label="ZX81" identifier="zx81" id="Wnn-nQ-gZ6">
<view key="view" id="bmd-gL-gzT">
<rect key="frame" x="10" y="7" width="400" height="76"/>
<rect key="frame" x="10" y="7" width="400" height="186"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5aO-UX-HnX">
<rect key="frame" x="108" y="47" width="116" height="25"/>
<rect key="frame" x="108" y="157" width="116" height="25"/>
<popUpButtonCell key="cell" type="push" title="Unexpanded" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="axesIndependently" inset="2" selectedItem="7QC-Ij-hES" id="d3W-Gl-3Mf">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@ -602,7 +602,7 @@ Gw
</popUpButtonCell>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8tU-73-XEE">
<rect key="frame" x="18" y="53" width="87" height="16"/>
<rect key="frame" x="18" y="163" width="87" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Memory Size:" id="z4b-oR-Yl2">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -622,24 +622,28 @@ Gw
</tabViewItem>
<tabViewItem label="ZX Spectrum" identifier="spectrum" id="HQv-oF-k8b">
<view key="view" id="bMx-F6-JUb">
<rect key="frame" x="10" y="7" width="400" height="76"/>
<rect key="frame" x="10" y="7" width="400" height="186"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gFZ-d4-WFv">
<rect key="frame" x="67" y="47" width="62" height="25"/>
<popUpButtonCell key="cell" type="push" title="+2a" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="21" imageScaling="axesIndependently" inset="2" selectedItem="Fo7-NL-Kv5" id="tYs-sA-oek">
<rect key="frame" x="67" y="157" width="76" height="25"/>
<popUpButtonCell key="cell" type="push" title="16kb" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" tag="16" imageScaling="axesIndependently" inset="2" selectedItem="Fo7-NL-Kv5" id="tYs-sA-oek">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="8lt-dk-zPr">
<items>
<menuItem title="+2a" state="on" tag="21" id="Fo7-NL-Kv5"/>
<menuItem title="16kb" tag="16" id="Fo7-NL-Kv5"/>
<menuItem title="48kb" tag="48" id="xks-Rv-Umd"/>
<menuItem title="128kb" tag="128" id="w8h-lY-JLX"/>
<menuItem title="+2" tag="2" id="Vvu-ua-pjg"/>
<menuItem title="+2a" state="on" tag="21" id="bFk-nC-Txe"/>
<menuItem title="+3" tag="3" id="jwx-fZ-vXp"/>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJ3-ma-Byy">
<rect key="frame" x="18" y="53" width="46" height="16"/>
<rect key="frame" x="18" y="163" width="46" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Model:" id="JId-Tp-LrE">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>

View File

@ -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

View File

@ -1258,9 +1258,13 @@ void MainWindow::start_spectrum() {
using Target = Analyser::Static::ZXSpectrum::Target;
auto target = std::make_unique<Target>();
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));

View File

@ -551,6 +551,26 @@
</item>
<item row="0" column="1">
<widget class="QComboBox" name="spectrumModelComboBox">
<item>
<property name="text">
<string>16kb</string>
</property>
</item>
<item>
<property name="text">
<string>48kb</string>
</property>
</item>
<item>
<property name="text">
<string>128kb</string>
</property>
</item>
<item>
<property name="text">
<string>+2</string>
</property>
</item>
<item>
<property name="text">
<string>+2a</string>

Binary file not shown.

BIN
ROMImages/ZXSpectrum/48.rom Normal file

Binary file not shown.

Binary file not shown.

View File

@ -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.