diff --git a/ClockReceiver/ClockReceiver.hpp b/ClockReceiver/ClockReceiver.hpp index 515d3216b..097b1c6db 100644 --- a/ClockReceiver/ClockReceiver.hpp +++ b/ClockReceiver/ClockReceiver.hpp @@ -95,9 +95,17 @@ template class WrappedInt { return *static_cast(this); } + inline T &operator &=(const T &rhs) { + length_ &= rhs.length_; + return *static_cast(this); + } + inline T operator +(const T &rhs) const { return T(length_ + rhs.length_); } inline T operator -(const T &rhs) const { return T(length_ - rhs.length_); } + inline T operator %(const T &rhs) const { return T(length_ % rhs.length_); } + inline T operator &(const T &rhs) const { return T(length_ & rhs.length_); } + inline T operator -() const { return T(- length_); } inline bool operator <(const T &rhs) const { return length_ < rhs.length_; } @@ -167,6 +175,17 @@ class HalfCycles: public WrappedInt { length_ &= 1; return result; } + + /*! + Severs from @c this the effect of dividing by @c divisor — @c this will end up with + the value of @c this modulo @c divisor and @c divided by @c divisor is returned. + */ + inline Cycles divide_cycles(const Cycles &divisor) { + HalfCycles half_divisor = HalfCycles(divisor); + Cycles result(length_ / half_divisor.length_); + length_ %= half_divisor.length_; + return result; + } }; /*! diff --git a/ClockReceiver/ForceInline.h b/ClockReceiver/ForceInline.h new file mode 100644 index 000000000..786e41c2d --- /dev/null +++ b/ClockReceiver/ForceInline.h @@ -0,0 +1,18 @@ +// +// ForceInline.h +// Clock Signal +// +// Created by Thomas Harte on 01/08/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#ifndef ForceInline_h +#define ForceInline_h + +#ifdef __GNUC__ +#define forceinline __attribute__((always_inline)) inline +#elif _MSC_VER +#define forceinline __forceinline +#endif + +#endif /* ForceInline_h */ diff --git a/Components/6845/CRTC6845.hpp b/Components/6845/CRTC6845.hpp new file mode 100644 index 000000000..13d1a2ffa --- /dev/null +++ b/Components/6845/CRTC6845.hpp @@ -0,0 +1,173 @@ +// +// CRTC6845.hpp +// Clock Signal +// +// Created by Thomas Harte on 31/07/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#ifndef CRTC6845_hpp +#define CRTC6845_hpp + +#include "../../ClockReceiver/ClockReceiver.hpp" + +#include +#include + +namespace Motorola { +namespace CRTC { + +struct BusState { + bool display_enable; + bool hsync; + bool vsync; + bool cursor; + uint16_t refresh_address; + uint16_t row_address; +}; + +class BusHandler { + public: + void perform_bus_cycle(const BusState &) {} +}; + +template class CRTC6845 { + public: + CRTC6845(T &bus_handler) : bus_handler_(bus_handler) {} + + void run_for(Cycles cycles) { + int cyles_remaining = cycles.as_int(); + while(cyles_remaining--) { + // check for end of horizontal sync + if(hsync_down_counter_) { + hsync_down_counter_--; + if(!hsync_down_counter_) { + bus_state_.hsync = false; + } + } + + // check for end of line + bool is_end_of_line = character_counter_ == registers_[0]; + + // increment counter + character_counter_++; + + // check for start of horizontal sync + if(character_counter_ == registers_[2]) { + hsync_down_counter_ = registers_[3] & 15; + if(hsync_down_counter_) bus_state_.hsync = true; + } + + // check for end of visible characters + if(character_counter_ == registers_[1]) { + bus_state_.refresh_address++; + character_is_visible_ = false; + } else { + // update refresh address + if(character_is_visible_) { + bus_state_.refresh_address++; + } + } + + // check for end-of-line + if(is_end_of_line) { + character_counter_ = 0; + character_is_visible_ = true; + + // check for end of vertical sync + if(vsync_down_counter_) { + vsync_down_counter_--; + if(!vsync_down_counter_) { + bus_state_.vsync = false; + } + } + + if(is_in_adjustment_period_) { + line_counter_++; + if(line_counter_ == registers_[5]) { + line_counter_ = 0; + is_in_adjustment_period_ = false; + line_is_visible_ = true; + line_address_ = (uint16_t)((registers_[12] << 8) | registers_[13]); + bus_state_.refresh_address = line_address_; + } + } else { + // advance vertical counter + if(bus_state_.row_address == registers_[9]) { + line_address_ = bus_state_.refresh_address; + bus_state_.row_address = 0; + line_counter_++; + + // check for end of visible lines + if(line_counter_ == registers_[6]) { + line_is_visible_ = false; + } + + // check for start of vertical sync + if(line_counter_ == registers_[7]) { + bus_state_.vsync = true; + vsync_down_counter_ = 16; // TODO + } + + // check for entry into the overflow area + if(line_counter_ == registers_[4]) { + if(registers_[5]) { + is_in_adjustment_period_ = true; + } else { + line_is_visible_ = true; + line_address_ = (uint16_t)((registers_[12] << 8) | registers_[13]); + bus_state_.refresh_address = line_address_; + } + bus_state_.row_address = 0; + line_counter_ = 0; + } + } else { + bus_state_.row_address++; + bus_state_.refresh_address = line_address_; + } + } + } + + bus_state_.display_enable = character_is_visible_ && line_is_visible_; + bus_handler_.perform_bus_cycle(bus_state_); + } + } + + void select_register(uint8_t r) { + selected_register_ = (int)r & 15; + } + + uint8_t get_status() { + return 0xff; + } + + uint8_t get_register() { + return registers_[selected_register_]; + } + + void set_register(uint8_t value) { + registers_[selected_register_] = value; + } + + private: + T &bus_handler_; + BusState bus_state_; + + uint8_t registers_[16]; + int selected_register_; + + uint8_t character_counter_; + uint8_t line_counter_; + + bool character_is_visible_, line_is_visible_; + + int hsync_down_counter_; + int vsync_down_counter_; + bool is_in_adjustment_period_; + uint16_t line_address_; +}; + +} +} + +#endif /* CRTC6845_hpp */ diff --git a/Components/8255/i8255.hpp b/Components/8255/i8255.hpp new file mode 100644 index 000000000..e188d669c --- /dev/null +++ b/Components/8255/i8255.hpp @@ -0,0 +1,72 @@ +// +// i8255.hpp +// Clock Signal +// +// Created by Thomas Harte on 01/08/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#ifndef i8255_hpp +#define i8255_hpp + +namespace Intel { +namespace i8255 { + +class PortHandler { + public: + void set_value(int port, uint8_t value) {} + uint8_t get_value(int port) { return 0xff; } +}; + +// TODO: most of the implementation below. Right now it just blindly passes data in all directions, +// ignoring operation mode. But at least it establishes proper ownership and hand-off of decision making. +template class i8255 { + public: + i8255(T &port_handler) : control_(0), outputs_{0, 0, 0}, port_handler_(port_handler) {} + + void set_register(int address, uint8_t value) { + switch(address & 3) { + case 0: outputs_[0] = value; port_handler_.set_value(0, value); break; + case 1: outputs_[1] = value; port_handler_.set_value(1, value); break; + case 2: outputs_[2] = value; port_handler_.set_value(2, value); break; + case 3: + if(value & 0x80) { + control_ = value; + } else { + if(value & 1) { + outputs_[2] |= 1 << ((value >> 1)&7); + } else { + outputs_[2] &= ~(1 << ((value >> 1)&7)); + } + } + update_outputs(); + break; + } + } + + uint8_t get_register(int address) { + switch(address & 3) { + case 0: return port_handler_.get_value(0); + case 1: return port_handler_.get_value(1); + case 2: return port_handler_.get_value(2); + case 3: return control_; + } + return 0xff; + } + + private: + void update_outputs() { + port_handler_.set_value(0, outputs_[0]); + port_handler_.set_value(1, outputs_[1]); + port_handler_.set_value(2, outputs_[2]); + } + + uint8_t control_; + uint8_t outputs_[3]; + T &port_handler_; +}; + +} +} + +#endif /* i8255_hpp */ diff --git a/Components/AY38910/AY38910.cpp b/Components/AY38910/AY38910.cpp index 22769b805..1a3f0b33e 100644 --- a/Components/AY38910/AY38910.cpp +++ b/Components/AY38910/AY38910.cpp @@ -166,6 +166,8 @@ void AY38910::evaluate_output_volume() { ); } +#pragma mark - Register manipulation + void AY38910::select_register(uint8_t r) { selected_register_ = r & 0xf; } @@ -227,12 +229,22 @@ uint8_t AY38910::get_register_value() { return registers_[selected_register_] | register_masks[selected_register_]; } +#pragma mark - Port handling + uint8_t AY38910::get_port_output(bool port_b) { return registers_[port_b ? 15 : 14]; } +void AY38910::set_port_input(bool port_b, uint8_t value) { + registers_[port_b ? 15 : 14] = value; + update_bus(); +} + +#pragma mark - Bus handling + void AY38910::set_data_input(uint8_t r) { data_input_ = r; + update_bus(); } uint8_t AY38910::get_data_output() { @@ -240,25 +252,25 @@ uint8_t AY38910::get_data_output() { } void AY38910::set_control_lines(ControlLines control_lines) { - ControlState new_state; switch((int)control_lines) { - default: new_state = Inactive; break; + default: control_state_ = Inactive; break; - case (int)(BCDIR | BC2 | BC1): - case BCDIR: - case BC1: new_state = LatchAddress; break; + case (int)(BDIR | BC2 | BC1): + case BDIR: + case BC1: control_state_ = LatchAddress; break; - case (int)(BC2 | BC1): new_state = Read; break; - case (int)(BCDIR | BC2): new_state = Write; break; + case (int)(BC2 | BC1): control_state_ = Read; break; + case (int)(BDIR | BC2): control_state_ = Write; break; } - if(new_state != control_state_) { - control_state_ = new_state; - switch(new_state) { - default: break; - case LatchAddress: select_register(data_input_); break; - case Write: set_register_value(data_input_); break; - case Read: data_output_ = get_register_value(); break; - } + update_bus(); +} + +void AY38910::update_bus() { + switch(control_state_) { + default: break; + case LatchAddress: select_register(data_input_); break; + case Write: set_register_value(data_input_); break; + case Read: data_output_ = get_register_value(); break; } } diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index 4b35e395a..8065990de 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -29,7 +29,7 @@ class AY38910: public ::Outputs::Filter { enum ControlLines { BC1 = (1 << 0), BC2 = (1 << 1), - BCDIR = (1 << 2) + BDIR = (1 << 2) }; /// Sets the value the AY would read from its data lines if it were not outputting. @@ -38,7 +38,7 @@ class AY38910: public ::Outputs::Filter { /// Gets the value that would appear on the data lines if only the AY is outputting. uint8_t get_data_output(); - /// Sets the + /// Sets the current control line state, as a bit field. void set_control_lines(ControlLines control_lines); /*! @@ -47,6 +47,12 @@ class AY38910: public ::Outputs::Filter { */ uint8_t get_port_output(bool port_b); + /*! + Sets the value that would appear on the requested interface port if it were in output mode. + @parameter port_b @c true to get the value for Port B, @c false to get the value for Port A. + */ + void set_port_input(bool port_b, uint8_t value); + // to satisfy ::Outputs::Speaker (included via ::Outputs::Filter; not for public consumption void get_samples(unsigned int number_of_samples, int16_t *target); @@ -88,6 +94,8 @@ class AY38910: public ::Outputs::Filter { int16_t output_volume_; inline void evaluate_output_volume(); + + inline void update_bus(); }; }; diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp new file mode 100644 index 000000000..f8efa5c6a --- /dev/null +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -0,0 +1,703 @@ +// +// AmstradCPC.cpp +// Clock Signal +// +// Created by Thomas Harte on 30/07/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#include "AmstradCPC.hpp" + +#include "../../Processors/Z80/Z80.hpp" + +#include "../../Components/8255/i8255.hpp" +#include "../../Components/AY38910/AY38910.hpp" +#include "../../Components/6845/CRTC6845.hpp" + +#include "../../Storage/Tape/Tape.hpp" + +using namespace AmstradCPC; + +/*! + Models the CPC's interrupt timer. Inputs are vsync, hsync, interrupt acknowledge and reset, and its output + is simply yes or no on whether an interupt is currently requested. Internally it uses a counter with a period + of 52 and occasionally adjusts or makes decisions based on bit 4. + + Hsync and vsync signals are expected to come directly from the CRTC; they are not decoded from a composite stream. +*/ +class InterruptTimer { + public: + InterruptTimer() : timer_(0), interrupt_request_(false) {} + + /*! + Indicates that a new hsync pulse has been recognised. Per documentation + difficulties, it is not presently clear to me whether this should be + the leading or trailing edge of horizontal sync. + */ + inline void signal_hsync() { + // Increment the timer and if it has hit 52 then reset it and + // set the interrupt request line to true. + timer_++; + if(timer_ == 52) { + timer_ = 0; + interrupt_request_ = true; + } + + // If a vertical sync has previously been indicated then after two + // further horizontal syncs the timer should either (i) set the interrupt + // line, if bit 4 is clear; or (ii) reset the timer. + if(reset_counter_) { + reset_counter_--; + if(!reset_counter_) { + if(timer_ < 32) { + interrupt_request_ = true; + } + timer_ = 0; + } + } + } + + /// Indicates the leading edge of a new vertical sync. + inline void signal_vsync() { + reset_counter_ = 2; + } + + /// Indicates that an interrupt acknowledge has been received from the Z80. + inline void signal_interrupt_acknowledge() { + interrupt_request_ = false; + timer_ &= ~32; + } + + /// @returns @c true if an interrupt is currently requested; @c false otherwise. + inline bool get_request() { + return interrupt_request_; + } + + /// Resets the timer. + inline void reset_count() { + timer_ = 0; + } + + private: + int reset_counter_; + bool interrupt_request_; + int timer_; +}; + +/*! + Provides a holder for an AY-3-8910 and its current cycles-since-updated count. + Therefore acts both to store an AY and to bookkeep this emulator's idiomatic + deferred clocking for this component. +*/ +class AYDeferrer { + public: + /// Constructs a new AY instance and sets its clock rate. + inline void setup_output() { + ay_.reset(new GI::AY38910); + ay_->set_input_rate(1000000); + } + + /// Destructs the AY. + inline void close_output() { + ay_.reset(); + } + + /// Adds @c half_cycles half cycles to the amount of time that has passed. + inline void run_for(HalfCycles half_cycles) { + cycles_since_update_ += half_cycles; + } + + /// Issues a request to the AY to perform all processing up to the current time. + inline void flush() { + ay_->run_for(cycles_since_update_.divide_cycles(Cycles(4))); + ay_->flush(); + } + + /// @returns the speaker the AY is using for output. + std::shared_ptr get_speaker() { + return ay_; + } + + /// @returns the AY itself. + GI::AY38910 *ay() { + return ay_.get(); + } + + private: + std::shared_ptr ay_; + HalfCycles cycles_since_update_; +}; + +/*! + Provides the mechanism of receipt for the CRTC outputs. In practice has the gate array's + video fetching and serialisation logic built in. So this is responsible for all video + generation and therefore owns details such as the current palette. +*/ +class CRTCBusHandler { + public: + CRTCBusHandler(uint8_t *ram, InterruptTimer &interrupt_timer) : + cycles_(0), + was_enabled_(false), + was_sync_(false), + pixel_data_(nullptr), + pixel_pointer_(nullptr), + was_hsync_(false), + ram_(ram), + interrupt_timer_(interrupt_timer), + pixel_divider_(1), + mode_(2), + next_mode_(2) { + build_mode_tables(); + } + + /*! + The CRTC entry function; takes the current bus state and determines what output + to produce based on the current palette and mode. + */ + inline void perform_bus_cycle(const Motorola::CRTC::BusState &state) { + // Sync is taken to override pixels, and is combined as a simple OR. + bool is_sync = state.hsync || state.vsync; + + // If a transition between sync/border/pixels just occurred, flush whatever was + // in progress to the CRT and reset counting. + if(state.display_enable != was_enabled_ || is_sync != was_sync_) { + if(was_sync_) { + crt_->output_sync(cycles_ * 16); + } else { + if(was_enabled_) { + if(cycles_) { + crt_->output_data(cycles_ * 16, pixel_divider_); + pixel_pointer_ = pixel_data_ = nullptr; + } + } else { + uint8_t *colour_pointer = (uint8_t *)crt_->allocate_write_area(1); + if(colour_pointer) *colour_pointer = border_; + crt_->output_level(cycles_ * 16); + } + } + + cycles_ = 0; + was_sync_ = is_sync; + was_enabled_ = state.display_enable; + } + + // increment cycles since state changed + cycles_++; + + // collect some more pixels if output is ongoing + if(!is_sync && state.display_enable) { + if(!pixel_data_) { + pixel_pointer_ = pixel_data_ = crt_->allocate_write_area(320); + } + if(pixel_pointer_) { + // the CPC shuffles output lines as: + // MA13 MA12 RA2 RA1 RA0 MA9 MA8 MA7 MA6 MA5 MA4 MA3 MA2 MA1 MA0 CCLK + // ... so form the real access address. + uint16_t address = + (uint16_t)( + ((state.refresh_address & 0x3ff) << 1) | + ((state.row_address & 0x7) << 11) | + ((state.refresh_address & 0x3000) << 2) + ); + + // fetch two bytes and translate into pixels + switch(mode_) { + case 0: + ((uint16_t *)pixel_pointer_)[0] = mode0_output_[ram_[address]]; + ((uint16_t *)pixel_pointer_)[1] = mode0_output_[ram_[address+1]]; + pixel_pointer_ += 4; + break; + + case 1: + ((uint32_t *)pixel_pointer_)[0] = mode1_output_[ram_[address]]; + ((uint32_t *)pixel_pointer_)[1] = mode1_output_[ram_[address+1]]; + pixel_pointer_ += 8; + break; + + case 2: + ((uint64_t *)pixel_pointer_)[0] = mode2_output_[ram_[address]]; + ((uint64_t *)pixel_pointer_)[1] = mode2_output_[ram_[address+1]]; + pixel_pointer_ += 16; + break; + + case 3: + ((uint16_t *)pixel_pointer_)[0] = mode3_output_[ram_[address]]; + ((uint16_t *)pixel_pointer_)[1] = mode3_output_[ram_[address+1]]; + pixel_pointer_ += 4; + break; + + } + + // flush the current buffer pixel if full; the CRTC allows many different display + // widths so it's not necessarily possible to predict the correct number in advance + // and using the upper bound could lead to inefficient behaviour + if(pixel_pointer_ == pixel_data_ + 320) { + crt_->output_data(cycles_ * 16, pixel_divider_); + pixel_pointer_ = pixel_data_ = nullptr; + cycles_ = 0; + } + } + } + + // check for a trailing hsync; if one occurred then that's the trigger potentially to change + // modes, and should also be sent on to the interrupt timer + if(was_hsync_ && !state.hsync) { + if(mode_ != next_mode_) { + mode_ = next_mode_; + switch(mode_) { + default: + case 0: pixel_divider_ = 4; break; + case 1: pixel_divider_ = 2; break; + case 2: pixel_divider_ = 1; break; + } + } + + interrupt_timer_.signal_hsync(); + } + + // check for a leading vsync; that also needs to be communicated to the interrupt timer + if(!was_vsync_ && state.vsync) { + interrupt_timer_.signal_vsync(); + } + + // update current state for edge detection next time around + was_vsync_ = state.vsync; + was_hsync_ = state.hsync; + } + + /// Constructs an appropriate CRT for video output. + void setup_output(float aspect_ratio) { + crt_.reset(new Outputs::CRT::CRT(1024, 16, Outputs::CRT::DisplayType::PAL50, 1)); + crt_->set_rgb_sampling_function( + "vec3 rgb_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate)" + "{" + "uint sample = texture(texID, coordinate).r;" + "return vec3(float((sample >> 4) & 3u), float((sample >> 2) & 3u), float(sample & 3u)) / 2.0;" + "}"); + crt_->set_visible_area(Outputs::CRT::Rect(0.05f, 0.05f, 0.9f, 0.9f)); + } + + /// Destructs the CRT. + void close_output() { + crt_.reset(); + } + + /// @returns the CRT. + std::shared_ptr get_crt() { + return crt_; + } + + /*! + Sets the next video mode. Per the documentation, mode changes take effect only at the end of line, + not immediately. So next means "as of the end of this line". + */ + void set_next_mode(int mode) { + next_mode_ = mode; + } + + /// @returns the current value of the CRTC's vertical sync output. + bool get_vsync() const { + return was_vsync_; + } + + /// Palette management: selects a pen to modify. + void select_pen(int pen) { + pen_ = pen; + } + + /// Palette management: sets the colour of the selected pen. + void set_colour(uint8_t colour) { + if(pen_ & 16) { + border_ = mapped_palette_value(colour); + // TODO: should flush any border currently in progress + } else { + palette_[pen_] = mapped_palette_value(colour); + // TODO: no need for a full regeneration, of every mode, every time + build_mode_tables(); + } + } + + private: + void build_mode_tables() { + for(int c = 0; c < 256; c++) { + // prepare mode 0 + uint8_t *mode0_pixels = (uint8_t *)&mode0_output_[c]; + mode0_pixels[0] = palette_[((c & 0x80) >> 7) | ((c & 0x20) >> 3) | ((c & 0x08) >> 2) | ((c & 0x02) << 2)]; + mode0_pixels[1] = palette_[((c & 0x40) >> 6) | ((c & 0x10) >> 2) | ((c & 0x04) >> 1) | ((c & 0x01) << 3)]; + + // prepare mode 1 + uint8_t *mode1_pixels = (uint8_t *)&mode1_output_[c]; + mode1_pixels[0] = palette_[((c & 0x80) >> 7) | ((c & 0x08) >> 2)]; + mode1_pixels[1] = palette_[((c & 0x40) >> 6) | ((c & 0x04) >> 1)]; + mode1_pixels[2] = palette_[((c & 0x20) >> 5) | ((c & 0x02) >> 0)]; + mode1_pixels[3] = palette_[((c & 0x10) >> 4) | ((c & 0x01) << 1)]; + + // prepare mode 2 + uint8_t *mode2_pixels = (uint8_t *)&mode2_output_[c]; + mode2_pixels[0] = palette_[((c & 0x80) >> 7)]; + mode2_pixels[1] = palette_[((c & 0x40) >> 6)]; + mode2_pixels[2] = palette_[((c & 0x20) >> 5)]; + mode2_pixels[3] = palette_[((c & 0x10) >> 4)]; + mode2_pixels[4] = palette_[((c & 0x08) >> 3)]; + mode2_pixels[5] = palette_[((c & 0x04) >> 2)]; + mode2_pixels[6] = palette_[((c & 0x03) >> 1)]; + mode2_pixels[7] = palette_[((c & 0x01) >> 0)]; + + // prepare mode 3 + uint8_t *mode3_pixels = (uint8_t *)&mode3_output_[c]; + mode3_pixels[0] = palette_[((c & 0x80) >> 7) | ((c & 0x08) >> 2)]; + mode3_pixels[1] = palette_[((c & 0x40) >> 6) | ((c & 0x04) >> 1)]; + } + } + + uint8_t mapped_palette_value(uint8_t colour) { +#define COL(r, g, b) (r << 4) | (g << 2) | b + static const uint8_t mapping[32] = { + COL(1, 1, 1), COL(1, 1, 1), COL(0, 2, 1), COL(2, 2, 1), + COL(0, 0, 1), COL(2, 0, 1), COL(0, 1, 1), COL(2, 1, 1), + COL(2, 0, 1), COL(2, 2, 1), COL(2, 2, 0), COL(2, 2, 2), + COL(2, 0, 0), COL(2, 0, 2), COL(2, 1, 0), COL(2, 1, 1), + COL(0, 0, 1), COL(0, 2, 1), COL(0, 2, 0), COL(0, 2, 2), + COL(0, 0, 0), COL(0, 0, 2), COL(0, 1, 0), COL(0, 1, 2), + COL(1, 0, 1), COL(1, 2, 1), COL(1, 2, 0), COL(1, 2, 2), + COL(1, 0, 0), COL(1, 0, 2), COL(1, 1, 0), COL(1, 1, 2), + }; +#undef COL + return mapping[colour]; + } + + unsigned int cycles_; + bool was_enabled_, was_sync_, was_hsync_, was_vsync_; + std::shared_ptr crt_; + uint8_t *pixel_data_, *pixel_pointer_; + + uint8_t *ram_; + + int next_mode_, mode_; + + unsigned int pixel_divider_; + uint16_t mode0_output_[256]; + uint32_t mode1_output_[256]; + uint64_t mode2_output_[256]; + uint16_t mode3_output_[256]; + + int pen_; + uint8_t palette_[16]; + uint8_t border_; + + InterruptTimer &interrupt_timer_; +}; + +/*! + Passively holds the current keyboard state. Keyboard state is modified in response + to external messages, so is handled by the machine, but is read by the i8255 port + handler, so is factored out. +*/ +struct KeyboardState { + KeyboardState() : rows{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} {} + uint8_t rows[10]; +}; + +/*! + Provides the mechanism of receipt for input and output of the 8255's various ports. +*/ +class i8255PortHandler : public Intel::i8255::PortHandler { + public: + i8255PortHandler( + const KeyboardState &key_state, + const CRTCBusHandler &crtc_bus_handler, + AYDeferrer &ay, + Storage::Tape::BinaryTapePlayer &tape_player) : + key_state_(key_state), + crtc_bus_handler_(crtc_bus_handler), + ay_(ay), + tape_player_(tape_player) {} + + /// The i8255 will call this to set a new output value of @c value for @c port. + void set_value(int port, uint8_t value) { + switch(port) { + case 0: + // Port A is connected to the AY's data bus. + ay_.ay()->set_data_input(value); + break; + case 1: + // Port B is an input only. So output goes nowehere. + break; + case 2: { + // The low four bits of the value sent to Port C select a keyboard line. + // At least for now, do a static push of the keyboard state here. So this + // is a capture. TODO: it should be a live connection. + int key_row = value & 15; + if(key_row < 10) { + ay_.ay()->set_port_input(false, key_state_.rows[key_row]); + } else { + ay_.ay()->set_port_input(false, 0xff); + } + + // Bit 4 sets the tape motor on or off. + tape_player_.set_motor_control((value & 0x10) ? true : false); + // Bit 5 sets the current tape output level + tape_player_.set_tape_output((value & 0x20) ? true : false); + + // Bits 6 and 7 set BDIR and BC1 for the AY. + ay_.ay()->set_control_lines( + (GI::AY38910::ControlLines)( + ((value & 0x80) ? GI::AY38910::BDIR : 0) | + ((value & 0x40) ? GI::AY38910::BC1 : 0) | + GI::AY38910::BC2 + )); + } break; + } + } + + /// The i8255 will call this to obtain a new input for @c port. + uint8_t get_value(int port) { + switch(port) { + case 0: return ay_.ay()->get_data_output(); // Port A is wired to the AY + case 1: return + (crtc_bus_handler_.get_vsync() ? 0x01 : 0x00) | // Bit 0 returns CRTC vsync. + (tape_player_.get_input() ? 0x80 : 0x00) | // Bit 7 returns cassette input. + 0x7e; // Bits unimplemented: + // + // Bit 6: printer ready (1 = not) + // Bit 5: the expansion port /EXP pin, so depends on connected hardware + // Bit 4: 50/60Hz switch (1 = 50Hz) + // Bits 1–3: distributor ID (111 = Amstrad) + default: return 0xff; + } + } + + private: + AYDeferrer &ay_; + const KeyboardState &key_state_; + const CRTCBusHandler &crtc_bus_handler_; + Storage::Tape::BinaryTapePlayer &tape_player_; +}; + +/*! + The actual Amstrad CPC implementation; tying the 8255, 6845 and AY to the Z80. +*/ +class ConcreteMachine: + public CPU::Z80::Processor, + public Machine { + public: + ConcreteMachine() : + crtc_counter_(HalfCycles(4)), // This starts the CRTC exactly out of phase with the memory accesses + crtc_(crtc_bus_handler_), + crtc_bus_handler_(ram_, interrupt_timer_), + i8255_(i8255_port_handler_), + i8255_port_handler_(key_state_, crtc_bus_handler_, ay_, tape_player_), + tape_player_(8000000) { + // primary clock is 4Mhz + set_clock_rate(4000000); + } + + /// The entry point for performing a partial Z80 machine cycle. + inline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) { + // Amstrad CPC timing scheme: assert WAIT for three out of four cycles + clock_offset_ = (clock_offset_ + cycle.length) & HalfCycles(7); + set_wait_line(clock_offset_ >= HalfCycles(2)); + + // Update the CRTC once every eight half cycles; aiming for half-cycle 4 as + // per the initial seed to the crtc_counter_, but any time in the final four + // will do as it's safe to conclude that nobody else has touched video RAM + // during that whole window + crtc_counter_ += cycle.length; + int crtc_cycles = crtc_counter_.divide(HalfCycles(8)).as_int(); + if(crtc_cycles) crtc_.run_for(Cycles(1)); + set_interrupt_line(interrupt_timer_.get_request()); + + // TODO (in the player, not here): adapt it to accept an input clock rate and + // run_for as HalfCycles + tape_player_.run_for(cycle.length.as_int()); + + // Pump the AY. + ay_.run_for(cycle.length); + + // Stop now if no action is strictly required. + if(!cycle.is_terminal()) return HalfCycles(0); + + uint16_t address = cycle.address ? *cycle.address : 0x0000; + switch(cycle.operation) { + case CPU::Z80::PartialMachineCycle::ReadOpcode: + case CPU::Z80::PartialMachineCycle::Read: + *cycle.value = read_pointers_[address >> 14][address & 16383]; + break; + + case CPU::Z80::PartialMachineCycle::Write: + write_pointers_[address >> 14][address & 16383] = *cycle.value; + break; + + case CPU::Z80::PartialMachineCycle::Output: + // Check for a gate array access. + if((address & 0xc000) == 0x4000) { + switch(*cycle.value >> 6) { + case 0: crtc_bus_handler_.select_pen(*cycle.value & 0x1f); break; + case 1: crtc_bus_handler_.set_colour(*cycle.value & 0x1f); break; + case 2: + // Perform ROM paging. + read_pointers_[0] = (*cycle.value & 4) ? &ram_[0] : os_.data(); + read_pointers_[3] = (*cycle.value & 8) ? &ram_[49152] : basic_.data(); + + // Reset the interrupt timer if requested. + if(*cycle.value & 15) interrupt_timer_.reset_count(); + + // Post the next mode. + crtc_bus_handler_.set_next_mode(*cycle.value & 3); + break; + case 3: printf("RAM paging?\n"); break; + } + } + + // Check for a CRTC access + if(!(address & 0x4000)) { + switch((address >> 8) & 3) { + case 0: crtc_.select_register(*cycle.value); break; + case 1: crtc_.set_register(*cycle.value); break; + case 2: case 3: printf("Illegal CRTC write?\n"); break; + } + } + + // Check for an 8255 PIO access + if(!(address & 0x800)) { + i8255_.set_register((address >> 8) & 3, *cycle.value); + } + break; + case CPU::Z80::PartialMachineCycle::Input: + // Default to nothing answering + *cycle.value = 0xff; + + // Check for a CRTC access + if(!(address & 0x4000)) { + switch((address >> 8) & 3) { + case 0: case 1: printf("Illegal CRTC read?\n"); break; + case 2: *cycle.value = crtc_.get_status(); break; + case 3: *cycle.value = crtc_.get_register(); break; + } + } + + // Check for a PIO access + if(!(address & 0x800)) { + *cycle.value = i8255_.get_register((address >> 8) & 3); + } + break; + + case CPU::Z80::PartialMachineCycle::Interrupt: + // Nothing is loaded onto the bus during an interrupt acknowledge, but + // the fact of the acknowledge needs to be posted on to the interrupt timer. + *cycle.value = 0xff; + interrupt_timer_.signal_interrupt_acknowledge(); + break; + + default: break; + } + + // This implementation doesn't use time-stuffing; once in-phase waits won't be longer + // than a single cycle so there's no real performance benefit to trying to find the + // next non-wait when a wait cycle comes in, and there'd be no benefit to reproducing + // the Z80's knowledge of where wait cycles occur here. + return HalfCycles(0); + } + + /// Another Z80 entry point; indicates that a partcular run request has concluded. + void flush() { + // Just flush the AY. + ay_.flush(); + } + + /// A CRTMachine function; indicates that outputs should be created now. + void setup_output(float aspect_ratio) { + crtc_bus_handler_.setup_output(aspect_ratio); + ay_.setup_output(); + } + + /// A CRTMachine function; indicates that outputs should be destroyed now. + void close_output() { + crtc_bus_handler_.close_output(); + ay_.close_output(); + } + + /// @returns the CRT in use. + std::shared_ptr get_crt() { + return crtc_bus_handler_.get_crt(); + } + + /// @returns the speaker in use. + std::shared_ptr get_speaker() { + return ay_.get_speaker(); + } + + /// Wires virtual-dispatched CRTMachine run_for requests to the static Z80 method. + void run_for(const Cycles cycles) { + CPU::Z80::Processor::run_for(cycles); + } + + /// The ConfigurationTarget entry point; should configure this meachine as described by @c target. + void configure_as_target(const StaticAnalyser::Target &target) { + // Establish reset memory map as per machine model (or, for now, as a hard-wired 464) + read_pointers_[0] = os_.data(); + read_pointers_[1] = &ram_[16384]; + read_pointers_[2] = &ram_[32768]; + read_pointers_[3] = basic_.data(); + + write_pointers_[0] = &ram_[0]; + write_pointers_[1] = &ram_[16384]; + write_pointers_[2] = &ram_[32768]; + write_pointers_[3] = &ram_[49152]; + + // If there are any tapes supplied, use the first of them. + if(!target.tapes.empty()) { + tape_player_.set_tape(target.tapes.front()); + } + } + + // See header; provides the system ROMs. + void set_rom(ROMType type, std::vector data) { + // Keep only the two ROMs that are currently of interest. + switch(type) { + case ROMType::OS464: os_ = data; break; + case ROMType::BASIC464: basic_ = data; break; + default: break; + } + } + + // See header; sets a key as either pressed or released. + void set_key_state(uint16_t key, bool isPressed) { + int line = key >> 4; + uint8_t mask = (uint8_t)(1 << (key & 7)); + if(isPressed) key_state_.rows[line] &= ~mask; else key_state_.rows[line] |= mask; + } + + // See header; sets all keys to released. + void clear_all_keys() { + memset(key_state_.rows, 0xff, 10); + } + + private: + CRTCBusHandler crtc_bus_handler_; + Motorola::CRTC::CRTC6845 crtc_; + + AYDeferrer ay_; + i8255PortHandler i8255_port_handler_; + Intel::i8255::i8255 i8255_; + + InterruptTimer interrupt_timer_; + Storage::Tape::BinaryTapePlayer tape_player_; + + HalfCycles clock_offset_; + HalfCycles crtc_counter_; + HalfCycles half_cycles_since_ay_update_; + + uint8_t ram_[65536]; + std::vector os_, basic_; + + uint8_t *read_pointers_[4]; + uint8_t *write_pointers_[4]; + + KeyboardState key_state_; +}; + +// See header; constructs and returns an instance of the Amstrad CPC. +Machine *Machine::AmstradCPC() { + return new ConcreteMachine; +} diff --git a/Machines/AmstradCPC/AmstradCPC.hpp b/Machines/AmstradCPC/AmstradCPC.hpp new file mode 100644 index 000000000..964d6b22a --- /dev/null +++ b/Machines/AmstradCPC/AmstradCPC.hpp @@ -0,0 +1,70 @@ +// +// AmstradCPC.hpp +// Clock Signal +// +// Created by Thomas Harte on 30/07/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#ifndef AmstradCPC_hpp +#define AmstradCPC_hpp + +#include "../ConfigurationTarget.hpp" +#include "../CRTMachine.hpp" + +namespace AmstradCPC { + +enum ROMType: uint8_t { + OS464, OS664, OS6128, + BASIC464, BASIC664, BASIC6128, + AMSDOS +}; + +enum Key: uint16_t { +#define Line(l, k1, k2, k3, k4, k5, k6, k7, k8) \ + k1 = (l << 4) | 0x07,\ + k2 = (l << 4) | 0x06,\ + k3 = (l << 4) | 0x05,\ + k4 = (l << 4) | 0x04,\ + k5 = (l << 4) | 0x03,\ + k6 = (l << 4) | 0x02,\ + k7 = (l << 4) | 0x01,\ + k8 = (l << 4) | 0x00, + + Line(0, KeyFDot, KeyEnter, KeyF3, KeyF6, KeyF9, KeyDown, KeyRight, KeyUp) + Line(1, KeyF0, KeyF2, KeyF1, KeyF5, KeyF8, KeyF7, KeyCopy, KeyLeft) + Line(2, KeyControl, KeyBackSlash, KeyShift, KeyF4, KeyRightSquareBracket, KeyReturn, KeyLeftSquareBracket, KeyClear) + Line(3, KeyFullStop, KeyForwardSlash, KeyColon, KeySemicolon, KeyP, KeyAt, KeyMinus, KeyCaret) + Line(4, KeyComma, KeyM, KeyK, KeyL, KeyI, KeyO, Key9, Key0) + Line(5, KeySpace, KeyN, KeyJ, KeyH, KeyY, KeyU, Key7, Key8) + Line(6, KeyV, KeyB, KeyF, KeyG, KeyT, KeyR, Key5, Key6) + Line(7, KeyX, KeyC, KeyD, KeyS, KeyW, KeyE, Key3, Key4) + Line(8, KeyZ, KeyCapsLock, KeyA, KeyTab, KeyQ, KeyEscape, Key2, Key1) + Line(9, KeyDelete, KeyJoy1Fire3, KeyJoy2Fire2, KeyJoy1Fire1, KeyJoy1Right, KeyJoy1Left, KeyJoy1Down, KeyJoy1Up) + +#undef Line +}; + +/*! + Models an Amstrad CPC, a CRT-outputting machine that can accept configuration targets. +*/ +class Machine: + public CRTMachine::Machine, + public ConfigurationTarget::Machine { + public: + /// Creates an returns an Amstrad CPC on the heap. + static Machine *AmstradCPC(); + + /// Sets the contents of rom @c type to @c data. Assumed to be a setup step; has no effect once a machine is running. + virtual void set_rom(ROMType type, std::vector data) = 0; + + /// Indicates that @c key is either pressed or released, according to @c is_pressed. + virtual void set_key_state(uint16_t key, bool is_pressed) = 0; + + /// Indicates that all keys are now released. + virtual void clear_all_keys() = 0; +}; + +} + +#endif /* AmstradCPC_hpp */ diff --git a/Machines/CRTMachine.hpp b/Machines/CRTMachine.hpp index 5ea8d856e..96f2ba7fd 100644 --- a/Machines/CRTMachine.hpp +++ b/Machines/CRTMachine.hpp @@ -22,7 +22,7 @@ namespace CRTMachine { */ class Machine { public: - Machine() : clock_is_unlimited_(false) {} + Machine() : clock_is_unlimited_(false), delegate_(nullptr) {} /*! Causes the machine to set up its CRT and, if it has one, speaker. The caller guarantees @@ -57,7 +57,7 @@ class Machine { virtual void machine_did_change_clock_rate(Machine *machine) = 0; virtual void machine_did_change_clock_is_unlimited(Machine *machine) = 0; }; - void set_delegate(Delegate *delegate) { this->delegate_ = delegate; } + void set_delegate(Delegate *delegate) { delegate_ = delegate; } protected: void set_clock_rate(double clock_rate) { diff --git a/Machines/Oric/Oric.cpp b/Machines/Oric/Oric.cpp index 10e4f6b7e..74160be0d 100644 --- a/Machines/Oric/Oric.cpp +++ b/Machines/Oric/Oric.cpp @@ -244,7 +244,7 @@ void Machine::VIA::run_for(const Cycles cycles) { void Machine::VIA::update_ay() { ay8910->run_for(cycles_since_ay_update_.flush()); - ay8910->set_control_lines( (GI::AY38910::ControlLines)((ay_bdir_ ? GI::AY38910::BCDIR : 0) | (ay_bc1_ ? GI::AY38910::BC1 : 0) | GI::AY38910::BC2)); + ay8910->set_control_lines( (GI::AY38910::ControlLines)((ay_bdir_ ? GI::AY38910::BDIR : 0) | (ay_bc1_ ? GI::AY38910::BC1 : 0) | GI::AY38910::BC2)); } #pragma mark - TapePlayer diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 2412376ca..ac1236c91 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -46,6 +46,10 @@ 4B30512D1D989E2200B4FED8 /* Drive.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B30512B1D989E2200B4FED8 /* Drive.cpp */; }; 4B3051301D98ACC600B4FED8 /* Plus3.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B30512E1D98ACC600B4FED8 /* Plus3.cpp */; }; 4B37EE821D7345A6006A09A4 /* BinaryDump.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE801D7345A6006A09A4 /* BinaryDump.cpp */; }; + 4B38F3441F2EB3E900D9235D /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B38F3421F2EB3E900D9235D /* StaticAnalyser.cpp */; }; + 4B38F3481F2EC11D00D9235D /* AmstradCPC.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B38F3461F2EC11D00D9235D /* AmstradCPC.cpp */; }; + 4B38F34C1F2EC3CA00D9235D /* CSAmstradCPC.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B38F34B1F2EC3CA00D9235D /* CSAmstradCPC.mm */; }; + 4B38F34F1F2EC6BA00D9235D /* AmstradCPCOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B38F34D1F2EC6BA00D9235D /* AmstradCPCOptions.xib */; }; 4B3940E71DA83C8300427841 /* AsyncTaskQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3940E51DA83C8300427841 /* AsyncTaskQueue.cpp */; }; 4B3BA0C31D318AEC005DD7A7 /* C1540Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BA0C21D318AEB005DD7A7 /* C1540Tests.swift */; }; 4B3BA0CE1D318B44005DD7A7 /* C1540Bridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BA0C61D318B44005DD7A7 /* C1540Bridge.mm */; }; @@ -532,6 +536,13 @@ 4B30512F1D98ACC600B4FED8 /* Plus3.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Plus3.hpp; path = Electron/Plus3.hpp; sourceTree = ""; }; 4B37EE801D7345A6006A09A4 /* BinaryDump.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BinaryDump.cpp; sourceTree = ""; }; 4B37EE811D7345A6006A09A4 /* BinaryDump.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = BinaryDump.hpp; sourceTree = ""; }; + 4B38F3421F2EB3E900D9235D /* StaticAnalyser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = StaticAnalyser.cpp; path = ../../StaticAnalyser/AmstradCPC/StaticAnalyser.cpp; sourceTree = ""; }; + 4B38F3431F2EB3E900D9235D /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = StaticAnalyser.hpp; path = ../../StaticAnalyser/AmstradCPC/StaticAnalyser.hpp; sourceTree = ""; }; + 4B38F3461F2EC11D00D9235D /* AmstradCPC.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = AmstradCPC.cpp; path = AmstradCPC/AmstradCPC.cpp; sourceTree = ""; }; + 4B38F3471F2EC11D00D9235D /* AmstradCPC.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = AmstradCPC.hpp; path = AmstradCPC/AmstradCPC.hpp; sourceTree = ""; }; + 4B38F34A1F2EC3CA00D9235D /* CSAmstradCPC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSAmstradCPC.h; sourceTree = ""; }; + 4B38F34B1F2EC3CA00D9235D /* CSAmstradCPC.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CSAmstradCPC.mm; sourceTree = ""; }; + 4B38F34E1F2EC6BA00D9235D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/AmstradCPCOptions.xib"; sourceTree = SOURCE_ROOT; }; 4B3940E51DA83C8300427841 /* AsyncTaskQueue.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = AsyncTaskQueue.cpp; path = ../../Concurrency/AsyncTaskQueue.cpp; sourceTree = ""; }; 4B3940E61DA83C8300427841 /* AsyncTaskQueue.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = AsyncTaskQueue.hpp; path = ../../Concurrency/AsyncTaskQueue.hpp; sourceTree = ""; }; 4B3BA0C21D318AEB005DD7A7 /* C1540Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = C1540Tests.swift; sourceTree = ""; }; @@ -644,6 +655,7 @@ 4BAB62B41D327F7E00DF5BA0 /* G64.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = G64.hpp; sourceTree = ""; }; 4BAB62B61D3302CA00DF5BA0 /* PCMTrack.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PCMTrack.cpp; sourceTree = ""; }; 4BAB62B71D3302CA00DF5BA0 /* PCMTrack.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PCMTrack.hpp; sourceTree = ""; }; + 4BB06B211F316A3F00600C7A /* ForceInline.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ForceInline.h; sourceTree = ""; }; 4BB17D4C1ED7909F00ABD1E1 /* tests.expected.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = tests.expected.json; path = FUSE/tests.expected.json; sourceTree = ""; }; 4BB17D4D1ED7909F00ABD1E1 /* tests.in.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = tests.in.json; path = FUSE/tests.in.json; sourceTree = ""; }; 4BB297E51B587D8300A49093 /* start */ = {isa = PBXFileReference; lastKnownFileType = file; path = " start"; sourceTree = ""; }; @@ -985,11 +997,13 @@ 4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CSBestEffortUpdater.m; path = Updater/CSBestEffortUpdater.m; sourceTree = ""; }; 4BD69F921D98760000243FE1 /* AcornADF.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AcornADF.cpp; sourceTree = ""; }; 4BD69F931D98760000243FE1 /* AcornADF.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = AcornADF.hpp; sourceTree = ""; }; + 4BD9137D1F311BC5009BCF85 /* i8255.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = i8255.hpp; path = 8255/i8255.hpp; sourceTree = ""; }; 4BDDBA981EF3451200347E61 /* Z80MachineCycleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Z80MachineCycleTests.swift; sourceTree = ""; }; 4BE77A2C1D84ADFB00BC3827 /* File.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = File.cpp; path = ../../StaticAnalyser/Commodore/File.cpp; sourceTree = ""; }; 4BE77A2D1D84ADFB00BC3827 /* File.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = File.hpp; path = ../../StaticAnalyser/Commodore/File.hpp; sourceTree = ""; }; 4BE7C9161E3D397100A5496D /* TIA.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TIA.cpp; sourceTree = ""; }; 4BE7C9171E3D397100A5496D /* TIA.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TIA.hpp; sourceTree = ""; }; + 4BE845201F2FF7F100A5EA22 /* CRTC6845.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = CRTC6845.hpp; path = 6845/CRTC6845.hpp; sourceTree = ""; }; 4BE9A6B01EDE293000CBCB47 /* zexdoc.com */ = {isa = PBXFileReference; lastKnownFileType = file; name = zexdoc.com; path = Zexall/zexdoc.com; sourceTree = ""; }; 4BEA525D1DF33323007E74F2 /* Tape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Tape.cpp; path = Electron/Tape.cpp; sourceTree = ""; }; 4BEA525F1DF333D8007E74F2 /* Tape.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Tape.hpp; path = Electron/Tape.hpp; sourceTree = ""; }; @@ -1190,11 +1204,13 @@ 4B2A53981D117D36003C6002 /* Wrappers */ = { isa = PBXGroup; children = ( + 4B38F34A1F2EC3CA00D9235D /* CSAmstradCPC.h */, 4B2A53991D117D36003C6002 /* CSAtari2600.h */, 4B2A539B1D117D36003C6002 /* CSElectron.h */, 4BCF1FA61DADC5250039D2E7 /* CSOric.h */, 4B2A539D1D117D36003C6002 /* CSVic20.h */, 4B14978D1EE4B4D200CE2596 /* CSZX8081.h */, + 4B38F34B1F2EC3CA00D9235D /* CSAmstradCPC.mm */, 4B2A539A1D117D36003C6002 /* CSAtari2600.mm */, 4B2A539C1D117D36003C6002 /* CSElectron.mm */, 4BCF1FA71DADC5250039D2E7 /* CSOric.mm */, @@ -1249,6 +1265,24 @@ name = Outputs; sourceTree = ""; }; + 4B38F3451F2EB41800D9235D /* AmstradCPC */ = { + isa = PBXGroup; + children = ( + 4B38F3421F2EB3E900D9235D /* StaticAnalyser.cpp */, + 4B38F3431F2EB3E900D9235D /* StaticAnalyser.hpp */, + ); + name = AmstradCPC; + sourceTree = ""; + }; + 4B38F3491F2EC12000D9235D /* AmstradCPC */ = { + isa = PBXGroup; + children = ( + 4B38F3461F2EC11D00D9235D /* AmstradCPC.cpp */, + 4B38F3471F2EC11D00D9235D /* AmstradCPC.hpp */, + ); + name = AmstradCPC; + sourceTree = ""; + }; 4B3940E81DA83C8700427841 /* Concurrency */ = { isa = PBXGroup; children = ( @@ -1330,6 +1364,7 @@ 4B2A332E1DB86869002876E3 /* OricOptionsPanel.swift */, 4B9CCDA01DA279CA0098B625 /* Vic20OptionsPanel.swift */, 4B95FA9C1F11893B0008E395 /* ZX8081OptionsPanel.swift */, + 4B38F34D1F2EC6BA00D9235D /* AmstradCPCOptions.xib */, 4B8FE2131DA19D5F0090D3CE /* Atari2600Options.xib */, 4B8FE2171DA19D5F0090D3CE /* ElectronOptions.xib */, 4B8FE2151DA19D5F0090D3CE /* MachineDocument.xib */, @@ -1911,6 +1946,7 @@ 4B8E4ECD1DCE483D003716C3 /* KeyboardMachine.hpp */, 4B2A33291DB8544D002876E3 /* MemoryFuzzer.hpp */, 4B1E85741D170228001EF87D /* Typer.hpp */, + 4B38F3491F2EC12000D9235D /* AmstradCPC */, 4B2E2D961C3A06EC00138695 /* Atari2600 */, 4B4DC81D1D2C2425003C5BF8 /* Commodore */, 4B2E2D9E1C3A070900138695 /* Electron */, @@ -1998,6 +2034,8 @@ 4B1E85791D174DEC001EF87D /* 6532 */, 4BC9DF4C1D04691600F44158 /* 6560 */, 4B4A762D1DB1A35C007AAE2E /* AY38910 */, + 4BE845221F2FF7F400A5EA22 /* 6845 */, + 4BD9137C1F3115AC009BCF85 /* 8255 */, ); name = Components; path = ../../Components; @@ -2086,6 +2124,14 @@ name = Updater; sourceTree = ""; }; + 4BD9137C1F3115AC009BCF85 /* 8255 */ = { + isa = PBXGroup; + children = ( + 4BD9137D1F311BC5009BCF85 /* i8255.hpp */, + ); + name = 8255; + sourceTree = ""; + }; 4BE5F85A1C3E1C2500C43F01 /* Resources */ = { isa = PBXGroup; children = ( @@ -2095,6 +2141,14 @@ path = Resources; sourceTree = ""; }; + 4BE845221F2FF7F400A5EA22 /* 6845 */ = { + isa = PBXGroup; + children = ( + 4BE845201F2FF7F100A5EA22 /* CRTC6845.hpp */, + ); + name = 6845; + sourceTree = ""; + }; 4BE9A6B21EDE294200CBCB47 /* Zexall */ = { isa = PBXGroup; children = ( @@ -2157,6 +2211,7 @@ 4B5A12581DD55873007A2231 /* Disassembler */, 4BCF1FAC1DADD41F0039D2E7 /* Oric */, 4B14978C1EE4AC6200CE2596 /* ZX80/81 */, + 4B38F3451F2EB41800D9235D /* AmstradCPC */, ); name = StaticAnalyser; sourceTree = ""; @@ -2165,6 +2220,7 @@ isa = PBXGroup; children = ( 4BF6606A1F281573002CB053 /* ClockReceiver.hpp */, + 4BB06B211F316A3F00600C7A /* ForceInline.h */, ); name = ClockReceiver; path = ../../ClockReceiver; @@ -2294,6 +2350,7 @@ 4BB73EAC1B587A5100552FC2 /* MainMenu.xib in Resources */, 4B8FE21D1DA19D5F0090D3CE /* ElectronOptions.xib in Resources */, 4B79E4461E3AF38600141F11 /* floppy525.png in Resources */, + 4B38F34F1F2EC6BA00D9235D /* AmstradCPCOptions.xib in Resources */, 4BC9DF451D044FCA00F44158 /* ROMImages in Resources */, 4B1497981EE4B97F00CE2596 /* ZX8081Options.xib in Resources */, ); @@ -2614,6 +2671,7 @@ 4B2BFDB21DAEF5FF001A68B8 /* Video.cpp in Sources */, 4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */, 4BC3B74F1CD194CC00F86E85 /* Shader.cpp in Sources */, + 4B38F3481F2EC11D00D9235D /* AmstradCPC.cpp in Sources */, 4B8FE2221DA19FB20090D3CE /* MachinePanel.swift in Sources */, 4BBB14311CD2CECE00BDB55C /* IntermediateShader.cpp in Sources */, 4BD5F1951D13528900631CD1 /* CSBestEffortUpdater.m in Sources */, @@ -2676,6 +2734,8 @@ 4B8805FE1DD02552003085B1 /* Tape.cpp in Sources */, 4B9CCDA11DA279CA0098B625 /* Vic20OptionsPanel.swift in Sources */, 4B8805F01DCFC99C003085B1 /* Acorn.cpp in Sources */, + 4B38F34C1F2EC3CA00D9235D /* CSAmstradCPC.mm in Sources */, + 4B38F3441F2EB3E900D9235D /* StaticAnalyser.cpp in Sources */, 4B3051301D98ACC600B4FED8 /* Plus3.cpp in Sources */, 4B30512D1D989E2200B4FED8 /* Drive.cpp in Sources */, 4BCA6CC81D9DD9F000C2D7B2 /* CommodoreROM.cpp in Sources */, @@ -2783,6 +2843,14 @@ name = OricOptions.xib; sourceTree = ""; }; + 4B38F34D1F2EC6BA00D9235D /* AmstradCPCOptions.xib */ = { + isa = PBXVariantGroup; + children = ( + 4B38F34E1F2EC6BA00D9235D /* Base */, + ); + name = AmstradCPCOptions.xib; + sourceTree = ""; + }; 4B8FE2131DA19D5F0090D3CE /* Atari2600Options.xib */ = { isa = PBXVariantGroup; children = ( diff --git a/OSBindings/Mac/Clock Signal/Base.lproj/AmstradCPCOptions.xib b/OSBindings/Mac/Clock Signal/Base.lproj/AmstradCPCOptions.xib new file mode 100644 index 000000000..39bd7024b --- /dev/null +++ b/OSBindings/Mac/Clock Signal/Base.lproj/AmstradCPCOptions.xib @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist index eb8a76d93..cd9bb6f73 100644 --- a/OSBindings/Mac/Clock Signal/Info.plist +++ b/OSBindings/Mac/Clock Signal/Info.plist @@ -224,6 +224,24 @@ Tape Image CFBundleTypeRole Viewer + LSTypeIsPackage + 0 + NSDocumentClass + $(PRODUCT_MODULE_NAME).MachineDocument + + + CFBundleTypeExtensions + + cdt + + CFBundleTypeIconFile + cassette + CFBundleTypeName + Amstrad CPC Tape Image + CFBundleTypeRole + Viewer + LSTypeIsPackage + 0 NSDocumentClass $(PRODUCT_MODULE_NAME).MachineDocument diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index 6ed1e1d79..9fbc7ffbb 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -43,8 +43,7 @@ struct MachineDelegate: CRTMachine::Machine::Delegate { - (instancetype)init { self = [super init]; - if(self) - { + if(self) { _machineDelegate.machine = self; self.machine->set_delegate(&_machineDelegate); _speakerDelegate.machine = self; diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm index 72a5dee94..7a3b12b09 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm @@ -14,6 +14,7 @@ #include "StaticAnalyser.hpp" +#import "CSAmstradCPC.h" #import "CSAtari2600.h" #import "CSElectron.h" #import "CSOric.h" @@ -22,16 +23,13 @@ #import "Clock_Signal-Swift.h" -@implementation CSStaticAnalyser -{ +@implementation CSStaticAnalyser { StaticAnalyser::Target _target; } -- (instancetype)initWithFileAtURL:(NSURL *)url -{ +- (instancetype)initWithFileAtURL:(NSURL *)url { self = [super init]; - if(self) - { + if(self) { std::list targets = StaticAnalyser::GetTargets([url fileSystemRepresentation]); if(!targets.size()) return nil; _target = targets.front(); @@ -42,34 +40,31 @@ return self; } -- (NSString *)optionsPanelNibName -{ - switch(_target.machine) - { - case StaticAnalyser::Target::Atari2600: return @"Atari2600Options"; - case StaticAnalyser::Target::Electron: return @"ElectronOptions"; - case StaticAnalyser::Target::Oric: return @"OricOptions"; - case StaticAnalyser::Target::Vic20: return @"Vic20Options"; - case StaticAnalyser::Target::ZX8081: return @"ZX8081Options"; +- (NSString *)optionsPanelNibName { + switch(_target.machine) { + case StaticAnalyser::Target::AmstradCPC: return @"AmstradCPCOptions"; + case StaticAnalyser::Target::Atari2600: return @"Atari2600Options"; + case StaticAnalyser::Target::Electron: return @"ElectronOptions"; + case StaticAnalyser::Target::Oric: return @"OricOptions"; + case StaticAnalyser::Target::Vic20: return @"Vic20Options"; + case StaticAnalyser::Target::ZX8081: return @"ZX8081Options"; default: return nil; } } -- (CSMachine *)newMachine -{ - switch(_target.machine) - { - case StaticAnalyser::Target::Atari2600: return [[CSAtari2600 alloc] init]; - case StaticAnalyser::Target::Electron: return [[CSElectron alloc] init]; - case StaticAnalyser::Target::Oric: return [[CSOric alloc] init]; - case StaticAnalyser::Target::Vic20: return [[CSVic20 alloc] init]; - case StaticAnalyser::Target::ZX8081: return [[CSZX8081 alloc] init]; +- (CSMachine *)newMachine { + switch(_target.machine) { + case StaticAnalyser::Target::AmstradCPC: return [[CSAmstradCPC alloc] init]; + case StaticAnalyser::Target::Atari2600: return [[CSAtari2600 alloc] init]; + case StaticAnalyser::Target::Electron: return [[CSElectron alloc] init]; + case StaticAnalyser::Target::Oric: return [[CSOric alloc] init]; + case StaticAnalyser::Target::Vic20: return [[CSVic20 alloc] init]; + case StaticAnalyser::Target::ZX8081: return [[CSZX8081 alloc] init]; default: return nil; } } -- (void)applyToMachine:(CSMachine *)machine -{ +- (void)applyToMachine:(CSMachine *)machine { [machine applyTarget:_target]; } diff --git a/OSBindings/Mac/Clock Signal/Machine/Wrappers/CSAmstradCPC.h b/OSBindings/Mac/Clock Signal/Machine/Wrappers/CSAmstradCPC.h new file mode 100644 index 000000000..1286a6046 --- /dev/null +++ b/OSBindings/Mac/Clock Signal/Machine/Wrappers/CSAmstradCPC.h @@ -0,0 +1,14 @@ +// +// CSAmstradCPC.h +// Clock Signal +// +// Created by Thomas Harte on 30/07/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#import "CSMachine.h" +#import "CSKeyboardMachine.h" + +@interface CSAmstradCPC : CSMachine + +@end diff --git a/OSBindings/Mac/Clock Signal/Machine/Wrappers/CSAmstradCPC.mm b/OSBindings/Mac/Clock Signal/Machine/Wrappers/CSAmstradCPC.mm new file mode 100644 index 000000000..ad42574bd --- /dev/null +++ b/OSBindings/Mac/Clock Signal/Machine/Wrappers/CSAmstradCPC.mm @@ -0,0 +1,146 @@ +// +// CSAmstradCPC.m +// Clock Signal +// +// Created by Thomas Harte on 30/07/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#import "CSAmstradCPC.h" + +#include "AmstradCPC.hpp" + +#import "CSMachine+Subclassing.h" +#import "NSData+StdVector.h" +#import "NSBundle+DataResource.h" + +@implementation CSAmstradCPC { + std::unique_ptr _amstradCPC; +} + +- (CRTMachine::Machine * const)machine { + if(!_amstradCPC) { + _amstradCPC.reset(AmstradCPC::Machine::AmstradCPC()); + } + return _amstradCPC.get(); +} + +- (instancetype)init { + self = [super init]; + if(self) { + [self machine]; + NSDictionary *roms = @{ + @(AmstradCPC::ROMType::OS464) : @"os464", + @(AmstradCPC::ROMType::OS664) : @"os664", + @(AmstradCPC::ROMType::OS6128) : @"os6128", + @(AmstradCPC::ROMType::BASIC464) : @"basic464", + @(AmstradCPC::ROMType::BASIC664) : @"basic664", + @(AmstradCPC::ROMType::BASIC6128) : @"basic6128", + @(AmstradCPC::ROMType::AMSDOS) : @"amsdos", + }; + + for(NSNumber *key in roms.allKeys) { + AmstradCPC::ROMType type = (AmstradCPC::ROMType)key.integerValue; + NSString *name = roms[key]; + NSData *data = [self rom:name]; + if(data) { + _amstradCPC->set_rom(type, data.stdVector8); + } else { + NSLog(@"Amstrad CPC ROM missing: %@", name); + } + } + } + return self; +} + +- (NSData *)rom:(NSString *)name { + return [[NSBundle mainBundle] dataForResource:name withExtension:@"rom" subdirectory:@"ROMImages/AmstradCPC"]; +} + +- (NSString *)userDefaultsPrefix { return @"amstradCPC"; } + +#pragma mark - Keyboard Mapping + +- (void)clearAllKeys { + @synchronized(self) { + _amstradCPC->clear_all_keys(); + } +} + +- (void)setKey:(uint16_t)key isPressed:(BOOL)isPressed { + @synchronized(self) { + switch(key) { + case VK_ANSI_0: _amstradCPC->set_key_state(AmstradCPC::Key::Key0, isPressed); break; + case VK_ANSI_1: _amstradCPC->set_key_state(AmstradCPC::Key::Key1, isPressed); break; + case VK_ANSI_2: _amstradCPC->set_key_state(AmstradCPC::Key::Key2, isPressed); break; + case VK_ANSI_3: _amstradCPC->set_key_state(AmstradCPC::Key::Key3, isPressed); break; + case VK_ANSI_4: _amstradCPC->set_key_state(AmstradCPC::Key::Key4, isPressed); break; + case VK_ANSI_5: _amstradCPC->set_key_state(AmstradCPC::Key::Key5, isPressed); break; + case VK_ANSI_6: _amstradCPC->set_key_state(AmstradCPC::Key::Key6, isPressed); break; + case VK_ANSI_7: _amstradCPC->set_key_state(AmstradCPC::Key::Key7, isPressed); break; + case VK_ANSI_8: _amstradCPC->set_key_state(AmstradCPC::Key::Key8, isPressed); break; + case VK_ANSI_9: _amstradCPC->set_key_state(AmstradCPC::Key::Key9, isPressed); break; + + case VK_ANSI_Q: _amstradCPC->set_key_state(AmstradCPC::Key::KeyQ, isPressed); break; + case VK_ANSI_W: _amstradCPC->set_key_state(AmstradCPC::Key::KeyW, isPressed); break; + case VK_ANSI_E: _amstradCPC->set_key_state(AmstradCPC::Key::KeyE, isPressed); break; + case VK_ANSI_R: _amstradCPC->set_key_state(AmstradCPC::Key::KeyR, isPressed); break; + case VK_ANSI_T: _amstradCPC->set_key_state(AmstradCPC::Key::KeyT, isPressed); break; + case VK_ANSI_Y: _amstradCPC->set_key_state(AmstradCPC::Key::KeyY, isPressed); break; + case VK_ANSI_U: _amstradCPC->set_key_state(AmstradCPC::Key::KeyU, isPressed); break; + case VK_ANSI_I: _amstradCPC->set_key_state(AmstradCPC::Key::KeyI, isPressed); break; + case VK_ANSI_O: _amstradCPC->set_key_state(AmstradCPC::Key::KeyO, isPressed); break; + case VK_ANSI_P: _amstradCPC->set_key_state(AmstradCPC::Key::KeyP, isPressed); break; + case VK_ANSI_A: _amstradCPC->set_key_state(AmstradCPC::Key::KeyA, isPressed); break; + case VK_ANSI_S: _amstradCPC->set_key_state(AmstradCPC::Key::KeyS, isPressed); break; + case VK_ANSI_D: _amstradCPC->set_key_state(AmstradCPC::Key::KeyD, isPressed); break; + case VK_ANSI_F: _amstradCPC->set_key_state(AmstradCPC::Key::KeyF, isPressed); break; + case VK_ANSI_G: _amstradCPC->set_key_state(AmstradCPC::Key::KeyG, isPressed); break; + case VK_ANSI_H: _amstradCPC->set_key_state(AmstradCPC::Key::KeyH, isPressed); break; + case VK_ANSI_J: _amstradCPC->set_key_state(AmstradCPC::Key::KeyJ, isPressed); break; + case VK_ANSI_K: _amstradCPC->set_key_state(AmstradCPC::Key::KeyK, isPressed); break; + case VK_ANSI_L: _amstradCPC->set_key_state(AmstradCPC::Key::KeyL, isPressed); break; + case VK_ANSI_Z: _amstradCPC->set_key_state(AmstradCPC::Key::KeyZ, isPressed); break; + case VK_ANSI_X: _amstradCPC->set_key_state(AmstradCPC::Key::KeyX, isPressed); break; + case VK_ANSI_C: _amstradCPC->set_key_state(AmstradCPC::Key::KeyC, isPressed); break; + case VK_ANSI_V: _amstradCPC->set_key_state(AmstradCPC::Key::KeyV, isPressed); break; + case VK_ANSI_B: _amstradCPC->set_key_state(AmstradCPC::Key::KeyB, isPressed); break; + case VK_ANSI_N: _amstradCPC->set_key_state(AmstradCPC::Key::KeyN, isPressed); break; + case VK_ANSI_M: _amstradCPC->set_key_state(AmstradCPC::Key::KeyM, isPressed); break; + + case VK_Space: _amstradCPC->set_key_state(AmstradCPC::Key::KeySpace, isPressed); break; + case VK_ANSI_Grave: _amstradCPC->set_key_state(AmstradCPC::Key::KeyCopy, isPressed); break; + case VK_Return: _amstradCPC->set_key_state(AmstradCPC::Key::KeyReturn, isPressed); break; + case VK_ANSI_Minus: _amstradCPC->set_key_state(AmstradCPC::Key::KeyMinus, isPressed); break; + + case VK_RightArrow: _amstradCPC->set_key_state(AmstradCPC::Key::KeyRight, isPressed); break; + case VK_LeftArrow: _amstradCPC->set_key_state(AmstradCPC::Key::KeyLeft, isPressed); break; + case VK_DownArrow: _amstradCPC->set_key_state(AmstradCPC::Key::KeyDown, isPressed); break; + case VK_UpArrow: _amstradCPC->set_key_state(AmstradCPC::Key::KeyUp, isPressed); break; + + case VK_Delete: _amstradCPC->set_key_state(AmstradCPC::Key::KeyDelete, isPressed); break; + case VK_Escape: _amstradCPC->set_key_state(AmstradCPC::Key::KeyEscape, isPressed); break; + + case VK_ANSI_Comma: _amstradCPC->set_key_state(AmstradCPC::Key::KeyComma, isPressed); break; + case VK_ANSI_Period: _amstradCPC->set_key_state(AmstradCPC::Key::KeyFullStop, isPressed); break; + + case VK_ANSI_Semicolon: + _amstradCPC->set_key_state(AmstradCPC::Key::KeySemicolon, isPressed); break; + case VK_ANSI_Quote: _amstradCPC->set_key_state(AmstradCPC::Key::KeyColon, isPressed); break; + + case VK_ANSI_Slash: _amstradCPC->set_key_state(AmstradCPC::Key::KeyForwardSlash, isPressed); break; + case VK_ANSI_Backslash: _amstradCPC->set_key_state(AmstradCPC::Key::KeyBackSlash, isPressed); break; + + case VK_Shift: _amstradCPC->set_key_state(AmstradCPC::Key::KeyShift, isPressed); break; + case VK_Control: _amstradCPC->set_key_state(AmstradCPC::Key::KeyControl, isPressed); break; + + case VK_F12: _amstradCPC->set_key_state(AmstradCPC::Key::KeyFDot, isPressed); break; + + default: +// printf("%02x\n", key); + break; + } + } +} + +@end diff --git a/Outputs/Speaker.hpp b/Outputs/Speaker.hpp index 7d9215f50..a64082d42 100644 --- a/Outputs/Speaker.hpp +++ b/Outputs/Speaker.hpp @@ -87,6 +87,7 @@ class Speaker { Ensures any deferred processing occurs now. */ void flush() { + if(!queued_functions_) return; std::shared_ptr>> queued_functions = queued_functions_; queued_functions_.reset(); _queue->enqueue([queued_functions] { diff --git a/ROMImages/AmstradCPC/readme.txt b/ROMImages/AmstradCPC/readme.txt new file mode 100644 index 000000000..ebe897eff --- /dev/null +++ b/ROMImages/AmstradCPC/readme.txt @@ -0,0 +1,11 @@ +ROM files would ordinarily go here; the copyright status of these is uncertain so they have not been included in this repository. Per http://www.worldofspectrum.org/permits/amstrad-roms.txt, Amstrad themselves allow redistribution but the status of Locomotive's input is unclear. + +Expected files: + +amsdos.rom +basic464.rom +basic664.rom +basic6128.rom +os464.rom +os664.rom +os6128.rom \ No newline at end of file diff --git a/StaticAnalyser/AmstradCPC/StaticAnalyser.cpp b/StaticAnalyser/AmstradCPC/StaticAnalyser.cpp new file mode 100644 index 000000000..b66621960 --- /dev/null +++ b/StaticAnalyser/AmstradCPC/StaticAnalyser.cpp @@ -0,0 +1,23 @@ +// +// AmstradCPC.cpp +// Clock Signal +// +// Created by Thomas Harte on 30/07/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#include "StaticAnalyser.hpp" + +void StaticAnalyser::AmstradCPC::AddTargets( + const std::list> &disks, + const std::list> &tapes, + const std::list> &cartridges, + std::list &destination) { + Target target; + target.machine = Target::AmstradCPC; + target.probability = 1.0; + target.disks = disks; + target.tapes = tapes; + target.cartridges = cartridges; + destination.push_back(target); +} diff --git a/StaticAnalyser/AmstradCPC/StaticAnalyser.hpp b/StaticAnalyser/AmstradCPC/StaticAnalyser.hpp new file mode 100644 index 000000000..f798f5bcd --- /dev/null +++ b/StaticAnalyser/AmstradCPC/StaticAnalyser.hpp @@ -0,0 +1,27 @@ +// +// StaticAnalyser.hpp +// Clock Signal +// +// Created by Thomas Harte on 30/07/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#ifndef StaticAnalyser_AmstradCPC_StaticAnalyser_hpp +#define StaticAnalyser_AmstradCPC_StaticAnalyser_hpp + +#include "../StaticAnalyser.hpp" + +namespace StaticAnalyser { +namespace AmstradCPC { + +void AddTargets( + const std::list> &disks, + const std::list> &tapes, + const std::list> &cartridges, + std::list &destination +); + +} +} + +#endif /* StaticAnalyser_AmstradCPC_StaticAnalyser_hpp */ diff --git a/StaticAnalyser/StaticAnalyser.cpp b/StaticAnalyser/StaticAnalyser.cpp index 0d437a692..ff1502617 100644 --- a/StaticAnalyser/StaticAnalyser.cpp +++ b/StaticAnalyser/StaticAnalyser.cpp @@ -12,6 +12,7 @@ // Analysers #include "Acorn/StaticAnalyser.hpp" +#include "AmstradCPC/StaticAnalyser.hpp" #include "Atari/StaticAnalyser.hpp" #include "Commodore/StaticAnalyser.hpp" #include "Oric/StaticAnalyser.hpp" @@ -40,30 +41,28 @@ typedef int TargetPlatformType; enum class TargetPlatform: TargetPlatformType { Acorn = 1 << 0, - Atari2600 = 1 << 1, - Commodore = 1 << 2, - Oric = 1 << 3, - ZX8081 = 1 << 4, + AmstradCPC = 1 << 1, + Atari2600 = 1 << 2, + Commodore = 1 << 3, + Oric = 1 << 4, + ZX8081 = 1 << 5, - AllTape = Acorn | Commodore | Oric | ZX8081, + AllTape = Acorn | Commodore | Oric | ZX8081 | AmstradCPC, }; using namespace StaticAnalyser; -std::list StaticAnalyser::GetTargets(const char *file_name) -{ +std::list StaticAnalyser::GetTargets(const char *file_name) { std::list targets; // Get the extension, if any; it will be assumed that extensions are reliable, so an extension is a broad-phase // test as to file format. const char *mixed_case_extension = strrchr(file_name, '.'); char *lowercase_extension = nullptr; - if(mixed_case_extension) - { + if(mixed_case_extension) { lowercase_extension = strdup(mixed_case_extension+1); char *parser = lowercase_extension; - while(*parser) - { + while(*parser) { *parser = (char)tolower(*parser); parser++; } @@ -86,18 +85,17 @@ std::list StaticAnalyser::GetTargets(const char *file_name) } catch(...) {} #define Format(extension, list, class, platforms) \ - if(!strcmp(lowercase_extension, extension)) \ - { \ + if(!strcmp(lowercase_extension, extension)) { \ TryInsert(list, class, platforms) \ } - if(lowercase_extension) - { + if(lowercase_extension) { Format("80", tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 80 Format("81", tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 81 Format("a26", cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600) // A26 Format("adf", disks, Disk::AcornADF, TargetPlatform::Acorn) // ADF Format("bin", cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600) // BIN + Format("cdt", tapes, Tape::TZX, TargetPlatform::AmstradCPC) // CDT Format("csw", tapes, Tape::CSW, TargetPlatform::AllTape) // CSW Format("d64", disks, Disk::D64, TargetPlatform::Commodore) // D64 Format("dsd", disks, Disk::SSD, TargetPlatform::Acorn) // DSD @@ -108,14 +106,11 @@ std::list StaticAnalyser::GetTargets(const char *file_name) Format("p81", tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P81 // PRG - if(!strcmp(lowercase_extension, "prg")) - { + if(!strcmp(lowercase_extension, "prg")) { // try instantiating as a ROM; failing that accept as a tape try { Insert(cartridges, Cartridge::PRG, TargetPlatform::Commodore) - } - catch(...) - { + } catch(...) { try { Insert(tapes, Tape::PRG, TargetPlatform::Commodore) } catch(...) {} @@ -135,11 +130,12 @@ std::list StaticAnalyser::GetTargets(const char *file_name) // Hand off to platform-specific determination of whether these things are actually compatible and, // if so, how to load them. (TODO) - if(potential_platforms & (TargetPlatformType)TargetPlatform::Acorn) Acorn::AddTargets(disks, tapes, cartridges, targets); - if(potential_platforms & (TargetPlatformType)TargetPlatform::Atari2600) Atari::AddTargets(disks, tapes, cartridges, targets); - if(potential_platforms & (TargetPlatformType)TargetPlatform::Commodore) Commodore::AddTargets(disks, tapes, cartridges, targets); - if(potential_platforms & (TargetPlatformType)TargetPlatform::Oric) Oric::AddTargets(disks, tapes, cartridges, targets); - if(potential_platforms & (TargetPlatformType)TargetPlatform::ZX8081) ZX8081::AddTargets(disks, tapes, cartridges, targets); + if(potential_platforms & (TargetPlatformType)TargetPlatform::Acorn) Acorn::AddTargets(disks, tapes, cartridges, targets); + if(potential_platforms & (TargetPlatformType)TargetPlatform::AmstradCPC) AmstradCPC::AddTargets(disks, tapes, cartridges, targets); + if(potential_platforms & (TargetPlatformType)TargetPlatform::Atari2600) Atari::AddTargets(disks, tapes, cartridges, targets); + if(potential_platforms & (TargetPlatformType)TargetPlatform::Commodore) Commodore::AddTargets(disks, tapes, cartridges, targets); + if(potential_platforms & (TargetPlatformType)TargetPlatform::Oric) Oric::AddTargets(disks, tapes, cartridges, targets); + if(potential_platforms & (TargetPlatformType)TargetPlatform::ZX8081) ZX8081::AddTargets(disks, tapes, cartridges, targets); free(lowercase_extension); } diff --git a/StaticAnalyser/StaticAnalyser.hpp b/StaticAnalyser/StaticAnalyser.hpp index 982dc1315..ac1aca5ee 100644 --- a/StaticAnalyser/StaticAnalyser.hpp +++ b/StaticAnalyser/StaticAnalyser.hpp @@ -52,10 +52,11 @@ enum class ZX8081MemoryModel { */ struct Target { enum { + AmstradCPC, Atari2600, Electron, - Vic20, Oric, + Vic20, ZX8081 } machine; float probability; diff --git a/Storage/Disk/DigitalPhaseLockedLoop.cpp b/Storage/Disk/DigitalPhaseLockedLoop.cpp index 9e99aa054..8348049c9 100644 --- a/Storage/Disk/DigitalPhaseLockedLoop.cpp +++ b/Storage/Disk/DigitalPhaseLockedLoop.cpp @@ -18,7 +18,8 @@ DigitalPhaseLockedLoop::DigitalPhaseLockedLoop(int clocks_per_bit, size_t length window_length_(clocks_per_bit), offset_history_pointer_(0), offset_history_(length_of_history, 0), - offset_(0) {} + offset_(0), + delegate_(nullptr) {} void DigitalPhaseLockedLoop::run_for(const Cycles cycles) { offset_ += cycles.as_int(); diff --git a/Storage/Tape/Formats/TZX.cpp b/Storage/Tape/Formats/TZX.cpp index 6814b8bcd..efa3b3073 100644 --- a/Storage/Tape/Formats/TZX.cpp +++ b/Storage/Tape/Formats/TZX.cpp @@ -56,12 +56,14 @@ void TZX::get_next_pulses() { return; } +// printf("TZX %ld\n", ftell(file_)); switch(chunk_id) { case 0x10: get_standard_speed_data_block(); break; case 0x11: get_turbo_speed_data_block(); break; case 0x12: get_pure_tone_data_block(); break; case 0x13: get_pulse_sequence(); break; case 0x19: get_generalised_data_block(); break; + case 0x20: get_pause(); break; case 0x30: { // Text description. Ripe for ignoring. @@ -160,33 +162,67 @@ void TZX::get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_pe } void TZX::get_standard_speed_data_block() { - __unused uint16_t pause_after_block = fgetc16le(); - uint16_t data_length = fgetc16le(); - if(!data_length) return; + DataBlock data_block; + data_block.length_of_pilot_pulse = 2168; + data_block.length_of_sync_first_pulse = 667; + data_block.length_of_sync_second_pulse = 735; + data_block.length_of_zero_bit_pulse = 855; + data_block.length_of_one_bit_pulse = 1710; + data_block.number_of_bits_in_final_byte = 8; + + data_block.pause_after_block = fgetc16le(); + data_block.data_length = fgetc16le(); + if(!data_block.data_length) return; uint8_t first_byte = (uint8_t)fgetc(file_); - __unused int pilot_tone_pulses = (first_byte < 128) ? 8063 : 3223; + data_block.length_of_pilot_tone = (first_byte < 128) ? 8063 : 3223; ungetc(first_byte, file_); - // TODO: output pilot_tone_pulses pulses - // TODO: output data_length bytes, in the Spectrum encoding - fseek(file_, data_length, SEEK_CUR); - // TODO: output a pause of length paused_after_block ms + get_data_block(data_block); } void TZX::get_turbo_speed_data_block() { - __unused uint16_t length_of_pilot_pulse = fgetc16le(); - __unused uint16_t length_of_sync_first_pulse = fgetc16le(); - __unused uint16_t length_of_sync_second_pulse = fgetc16le(); - __unused uint16_t length_of_zero_bit_pulse = fgetc16le(); - __unused uint16_t length_of_one_bit_pulse = fgetc16le(); - __unused uint16_t length_of_pilot_tone = fgetc16le(); - __unused uint8_t number_of_bits_in_final_byte = (uint8_t)fgetc(file_); - __unused uint16_t pause_after_block = fgetc16le(); - uint16_t data_length = fgetc16le(); + DataBlock data_block; + data_block.length_of_pilot_pulse = fgetc16le(); + data_block.length_of_sync_first_pulse = fgetc16le(); + data_block.length_of_sync_second_pulse = fgetc16le(); + data_block.length_of_zero_bit_pulse = fgetc16le(); + data_block.length_of_one_bit_pulse = fgetc16le(); + data_block.length_of_pilot_tone = fgetc16le(); + data_block.number_of_bits_in_final_byte = (uint8_t)fgetc(file_); + data_block.pause_after_block = fgetc16le(); + data_block.data_length = fgetc16le(); + data_block.data_length |= (long)(fgetc(file_) << 16); - // TODO output as described - fseek(file_, data_length, SEEK_CUR); + get_data_block(data_block); +} + +void TZX::get_data_block(const DataBlock &data_block) { + // Output pilot tone. + for(unsigned int c = 0; c < data_block.length_of_pilot_tone; c++) { + post_pulse(data_block.length_of_pilot_pulse); + } + + // Output sync pulses. + post_pulse(data_block.length_of_sync_first_pulse); + post_pulse(data_block.length_of_sync_second_pulse); + + // Output data. + for(unsigned int c = 0; c < data_block.data_length; c++) { + uint8_t next_byte = (uint8_t)fgetc(file_); + + unsigned int bits = (c != data_block.data_length-1) ? 8 : data_block.number_of_bits_in_final_byte; + while(bits--) { + unsigned int pulse_length = (next_byte & 0x80) ? data_block.length_of_one_bit_pulse : data_block.length_of_zero_bit_pulse; + next_byte <<= 1; + + post_pulse(pulse_length); + post_pulse(pulse_length); + } + } + + // Output gap. + post_gap(data_block.pause_after_block); } void TZX::get_pure_tone_data_block() { @@ -203,6 +239,15 @@ void TZX::get_pulse_sequence() { } } +void TZX::get_pause() { + uint16_t duration = fgetc16le(); + if(!duration) { + // TODO (maybe): post a 'pause the tape' suggestion + } else { + post_gap(duration); + } +} + #pragma mark - Output void TZX::post_pulse(unsigned int length) { diff --git a/Storage/Tape/Formats/TZX.hpp b/Storage/Tape/Formats/TZX.hpp index d0a3eedfd..aeedf8ce9 100644 --- a/Storage/Tape/Formats/TZX.hpp +++ b/Storage/Tape/Formats/TZX.hpp @@ -42,8 +42,22 @@ class TZX: public PulseQueuedTape, public Storage::FileHolder { void get_pure_tone_data_block(); void get_pulse_sequence(); void get_generalised_data_block(); + void get_pause(); + + struct DataBlock { + unsigned int length_of_pilot_pulse; + unsigned int length_of_sync_first_pulse; + unsigned int length_of_sync_second_pulse; + unsigned int length_of_zero_bit_pulse; + unsigned int length_of_one_bit_pulse; + unsigned int length_of_pilot_tone; + unsigned int number_of_bits_in_final_byte; + unsigned int pause_after_block; + long data_length; + }; void get_generalised_segment(uint32_t output_symbols, uint8_t max_pulses_per_symbol, uint8_t number_of_symbols, bool is_data); + void get_data_block(const DataBlock &); void post_pulse(unsigned int length); void post_gap(unsigned int milliseconds); diff --git a/Storage/Tape/Parsers/Acorn.cpp b/Storage/Tape/Parsers/Acorn.cpp index 3cf875198..519fa3fe7 100644 --- a/Storage/Tape/Parsers/Acorn.cpp +++ b/Storage/Tape/Parsers/Acorn.cpp @@ -69,7 +69,8 @@ Shifter::Shifter() : pll_(PLLClockRate / 4800, 15), was_high_(false), input_pattern_(0), - input_bit_counter_(0) { + input_bit_counter_(0), + delegate_(nullptr) { pll_.set_delegate(this); } diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index f59314889..280b02100 100644 --- a/Storage/Tape/Tape.cpp +++ b/Storage/Tape/Tape.cpp @@ -110,7 +110,7 @@ void TapePlayer::process_next_event() { #pragma mark - Binary Player BinaryTapePlayer::BinaryTapePlayer(unsigned int input_clock_rate) : - TapePlayer(input_clock_rate), motor_is_running_(false), input_level_(false) + TapePlayer(input_clock_rate), motor_is_running_(false), input_level_(false), delegate_(nullptr) {} void BinaryTapePlayer::set_motor_control(bool enabled) {