diff --git a/Machines/MemoryFuzzer.cpp b/Machines/MemoryFuzzer.cpp index 60dd8e388..467d27045 100644 --- a/Machines/MemoryFuzzer.cpp +++ b/Machines/MemoryFuzzer.cpp @@ -22,3 +22,7 @@ void Memory::Fuzz(uint8_t *buffer, size_t size) { buffer[c] = (uint8_t)(std::rand() >> shift); } } + +void Memory::Fuzz(std::vector &buffer) { + Fuzz(buffer.data(), buffer.size()); +} diff --git a/Machines/MemoryFuzzer.hpp b/Machines/MemoryFuzzer.hpp index 648915033..3de6bdcbd 100644 --- a/Machines/MemoryFuzzer.hpp +++ b/Machines/MemoryFuzzer.hpp @@ -11,10 +11,12 @@ #include #include +#include namespace Memory { void Fuzz(uint8_t *buffer, size_t size); +void Fuzz(std::vector &buffer); } diff --git a/Machines/ZX8081/Video.cpp b/Machines/ZX8081/Video.cpp new file mode 100644 index 000000000..5102cdffd --- /dev/null +++ b/Machines/ZX8081/Video.cpp @@ -0,0 +1,106 @@ +// +// Video.cpp +// Clock Signal +// +// Created by Thomas Harte on 06/06/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#include "Video.hpp" + +using namespace ZX8081; + +Video::Video() : + crt_(new Outputs::CRT::CRT(207 * 2, 1, Outputs::CRT::DisplayType::PAL50, 1)), + line_data_(nullptr), + line_data_pointer_(nullptr), + cycles_since_update_(0), + sync_(false) { + + // Set a composite sampling function that assumes 8bpp input grayscale. + // TODO: lessen this to 1bpp. + crt_->set_composite_sampling_function( + "float composite_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate, float phase, float amplitude)" + "{" + "return float(texture(texID, coordinate).r) / 255.0;" + "}"); + + // Show only the centre 80% of the TV frame. + crt_->set_visible_area(Outputs::CRT::Rect(0.1f, 0.1f, 0.8f, 0.8f)); +} + +void Video::run_for_cycles(int number_of_cycles) { + // Just keep a running total of the amount of time that remains owed to the CRT. + cycles_since_update_ += (unsigned int)number_of_cycles << 1; +} + +void Video::flush() { + flush(sync_); +} + +void Video::flush(bool next_sync) { + if(sync_) { + // If in sync, that takes priority. Output the proper amount of sync. + crt_->output_sync(cycles_since_update_); + } else { + // If not presently in sync, then... + + if(line_data_) { + // If there is output data queued, output it either if it's being interrupted by + // sync, or if we're past its end anyway. Otherwise let it be. + unsigned int data_length = (unsigned int)(line_data_pointer_ - line_data_); + if(data_length < cycles_since_update_ || next_sync) { + crt_->output_data(data_length, 1); + line_data_pointer_ = line_data_ = nullptr; + cycles_since_update_ -= data_length; + } else return; + } + + // Any pending pixels being dealt with, pad with the white level. + uint8_t *colour_pointer = (uint8_t *)crt_->allocate_write_area(1); + if(colour_pointer) *colour_pointer = 0xff; + crt_->output_level(cycles_since_update_); + } + + cycles_since_update_ = 0; +} + +void Video::set_sync(bool sync) { + // Do nothing if sync hasn't changed. + if(sync_ == sync) return; + + // Complete whatever was being drawn, and update sync. + flush(sync); + sync_ = sync; +} + +void Video::output_byte(uint8_t byte) { + // Complete whatever was going on. + flush(); + + // Grab a buffer if one isn't already available. + if(!line_data_) { + line_data_pointer_ = line_data_ = crt_->allocate_write_area(320); + } + + // If a buffer was obtained, serialise the new pixels. + if(line_data_) { + uint8_t mask = 0x80; + for(int c = 0; c < 8; c++) { + line_data_pointer_[c] = (byte & mask) ? 0xff : 0x00; + mask >>= 1; + } + line_data_pointer_ += 8; + + // If that fills the buffer, output it now. + if(line_data_pointer_ - line_data_ == 320) { + crt_->output_data(320, 1); + line_data_pointer_ = line_data_ = nullptr; + cycles_since_update_ -= 160; + } + } +} + +std::shared_ptr Video::get_crt() { + return crt_; +} diff --git a/Machines/ZX8081/Video.hpp b/Machines/ZX8081/Video.hpp new file mode 100644 index 000000000..9c63021ba --- /dev/null +++ b/Machines/ZX8081/Video.hpp @@ -0,0 +1,54 @@ +// +// Video.hpp +// Clock Signal +// +// Created by Thomas Harte on 06/06/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#ifndef Machines_ZX8081_Video_hpp +#define Machines_ZX8081_Video_hpp + +#include "../../Outputs/CRT/CRT.hpp" + +namespace ZX8081 { + +/*! + Packages a ZX80/81-style video feed into a CRT-compatible waveform. + + While sync is active, this feed will output the sync level. + + While sync is inactive, this feed will output the white level unless it is supplied + with a byte to output. When a byte is supplied for output, it will be interpreted as + a 1-bit graphic and output over the next 4 cycles, picking between the white level + and the black level. +*/ +class Video { + public: + /// Constructs an instance of the video feed; a CRT is also created. + Video(); + /// @returns The CRT this video feed is feeding. + std::shared_ptr get_crt(); + + /// Advances time by @c number_of_cycles cycles. + void run_for_cycles(int number_of_cycles); + /// Forces output to catch up to the current output position. + void flush(); + + /// Sets the current sync output. + void set_sync(bool sync); + /// Causes @c byte to be serialised into pixels and output over the next four cycles. + void output_byte(uint8_t byte); + + private: + bool sync_; + uint8_t *line_data_, *line_data_pointer_; + unsigned int cycles_since_update_; + std::shared_ptr crt_; + + void flush(bool next_sync); +}; + +} + +#endif /* Video_hpp */ diff --git a/Machines/ZX8081/ZX8081.cpp b/Machines/ZX8081/ZX8081.cpp new file mode 100644 index 000000000..680b78817 --- /dev/null +++ b/Machines/ZX8081/ZX8081.cpp @@ -0,0 +1,252 @@ +// +// ZX8081.cpp +// Clock Signal +// +// Created by Thomas Harte on 04/06/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#include "ZX8081.hpp" + +#include "../MemoryFuzzer.hpp" + +using namespace ZX8081; + +namespace { + // The clock rate is 3.25Mhz. + const unsigned int ZX8081ClockRate = 3250000; +} + +Machine::Machine() : + vsync_(false), + hsync_(false), + nmi_is_enabled_(false), + tape_player_(ZX8081ClockRate) { + set_clock_rate(ZX8081ClockRate); + tape_player_.set_motor_control(true); + clear_all_keys(); +} + +int Machine::perform_machine_cycle(const CPU::Z80::MachineCycle &cycle) { + int wait_cycles = 0; + + int previous_counter = horizontal_counter_; + horizontal_counter_ += cycle.length; + + if(previous_counter < vsync_start_cycle_ && horizontal_counter_ >= vsync_start_cycle_) { + video_->run_for_cycles(vsync_start_cycle_ - previous_counter); + set_hsync(true); + if(nmi_is_enabled_) { + set_non_maskable_interrupt_line(true); + if(!get_halt_line()) { + wait_cycles = vsync_end_cycle_ - horizontal_counter_; + } + } + video_->run_for_cycles(horizontal_counter_ - vsync_start_cycle_ + wait_cycles); + } else if(previous_counter <= vsync_end_cycle_ && horizontal_counter_ > vsync_end_cycle_) { + video_->run_for_cycles(vsync_end_cycle_ - previous_counter); + set_hsync(false); + if(nmi_is_enabled_) set_non_maskable_interrupt_line(false); + video_->run_for_cycles(horizontal_counter_ - vsync_end_cycle_); + } else { + video_->run_for_cycles(cycle.length); + } + + horizontal_counter_ += wait_cycles; + if(is_zx81_) horizontal_counter_ %= 207; + +// tape_player_.run_for_cycles(cycle.length + wait_cycles); + + uint16_t refresh = 0; + uint16_t address = cycle.address ? *cycle.address : 0; + switch(cycle.operation) { + case CPU::Z80::BusOperation::Output: + set_vsync(false); + line_counter_ = 0; + + switch(address & 7) { + default: break; + case 0x5: nmi_is_enabled_ = false; break; + case 0x6: nmi_is_enabled_ = is_zx81_; break; + } + break; + + case CPU::Z80::BusOperation::Input: { + uint8_t value = 0xff; + if(!(address&1)) { + set_vsync(true); + + uint16_t mask = 0x100; + for(int c = 0; c < 8; c++) { + if(!(address & mask)) value &= key_states_[c]; + mask <<= 1; + } + + value &= ~(tape_player_.get_input() ? 0x00 : 0x80); + } + *cycle.value = value; + } break; + + case CPU::Z80::BusOperation::Interrupt: + line_counter_ = (line_counter_ + 1) & 7; + *cycle.value = 0xff; + horizontal_counter_ = 0; + break; + + case CPU::Z80::BusOperation::ReadOpcode: + // The ZX80 and 81 signal an interrupt while refresh is active and bit 6 of the refresh + // address is low. The Z80 signals a refresh, providing the refresh address during the + // final two cycles of an opcode fetch. Therefore communicate a transient signalling + // of the IRQ line if necessary. + refresh = get_value_of_register(CPU::Z80::Register::Refresh); + set_interrupt_line(!(refresh & 0x40), -2); + set_interrupt_line(false); + + // Check for use of the fast tape hack. + if(address == tape_trap_address_) { // TODO: && fast_tape_hack_enabled_ + int next_byte = parser_.get_next_byte(tape_player_.get_tape()); + if(next_byte != -1) { + uint16_t hl = get_value_of_register(CPU::Z80::Register::HL); + ram_[hl & ram_mask_] = (uint8_t)next_byte; + *cycle.value = 0x00; + set_value_of_register(CPU::Z80::Register::ProgramCounter, tape_return_address_ - 1); + return 0; + } + } + + case CPU::Z80::BusOperation::Read: + if(address < ram_base_) { + *cycle.value = rom_[address & rom_mask_]; + } else { + uint8_t value = ram_[address & ram_mask_]; + + // If this is an M1 cycle reading from above the 32kb mark and HALT is not + // currently active, perform a video output and return a NOP. Otherwise, + // just return the value as read. + if(cycle.operation == CPU::Z80::BusOperation::ReadOpcode && address&0x8000 && !(value & 0x40) && !get_halt_line()) { + size_t char_address = (size_t)((refresh & 0xff00) | ((value & 0x3f) << 3) | line_counter_); + if(char_address < ram_base_) { + uint8_t mask = (value & 0x80) ? 0x00 : 0xff; + value = rom_[char_address & rom_mask_] ^ mask; + } + + video_->output_byte(value); + *cycle.value = 0; + } else *cycle.value = value; + } + break; + + case CPU::Z80::BusOperation::Write: + if(address >= ram_base_) { + ram_[address & ram_mask_] = *cycle.value; + } + break; + + default: break; + } + + return wait_cycles; +} + +void Machine::flush() { + video_->flush(); +} + +void Machine::setup_output(float aspect_ratio) { + video_.reset(new Video); +} + +void Machine::close_output() { + video_.reset(); +} + +std::shared_ptr Machine::get_crt() { + return video_->get_crt(); +} + +std::shared_ptr Machine::get_speaker() { + return nullptr; +} + +void Machine::run_for_cycles(int number_of_cycles) { + CPU::Z80::Processor::run_for_cycles(number_of_cycles); +} + +void Machine::configure_as_target(const StaticAnalyser::Target &target) { + is_zx81_ = target.zx8081.isZX81; + if(is_zx81_) { + rom_ = zx81_rom_; + tape_trap_address_ = 0x37c; + tape_return_address_ = 0x380; + vsync_start_cycle_ = 13; + vsync_end_cycle_ = 33; + vsync_start_cycle_ = 16; + vsync_end_cycle_ = 32; + } else { + rom_ = zx80_rom_; + tape_trap_address_ = 0x220; + tape_return_address_ = 0x248; + vsync_start_cycle_ = 13; + vsync_end_cycle_ = 33; + } + rom_mask_ = (uint16_t)(rom_.size() - 1); + + switch(target.zx8081.memory_model) { + case StaticAnalyser::ZX8081MemoryModel::Unexpanded: + ram_.resize(1024); + ram_base_ = 16384; + ram_mask_ = 1023; + break; + case StaticAnalyser::ZX8081MemoryModel::SixteenKB: + ram_.resize(16384); + ram_base_ = 16384; + ram_mask_ = 16383; + break; + case StaticAnalyser::ZX8081MemoryModel::SixtyFourKB: + ram_.resize(65536); + ram_base_ = 8192; + ram_mask_ = 65535; + break; + } + Memory::Fuzz(ram_); + + if(target.tapes.size()) { + tape_player_.set_tape(target.tapes.front()); + } +} + +void Machine::set_rom(ROMType type, std::vector data) { + switch(type) { + case ZX80: zx80_rom_ = data; break; + case ZX81: zx81_rom_ = data; break; + } +} + +#pragma mark - Video + +void Machine::set_vsync(bool sync) { + vsync_ = sync; + update_sync(); +} + +void Machine::set_hsync(bool sync) { + hsync_ = sync; + update_sync(); +} + +void Machine::update_sync() { + video_->set_sync(vsync_ || hsync_); +} + +#pragma mark - Keyboard + +void Machine::set_key_state(uint16_t key, bool isPressed) { + if(isPressed) + key_states_[key >> 8] &= (uint8_t)(~key); + else + key_states_[key >> 8] |= (uint8_t)key; +} + +void Machine::clear_all_keys() { + memset(key_states_, 0xff, 8); +} diff --git a/Machines/ZX8081/ZX8081.hpp b/Machines/ZX8081/ZX8081.hpp new file mode 100644 index 000000000..5778b85fe --- /dev/null +++ b/Machines/ZX8081/ZX8081.hpp @@ -0,0 +1,96 @@ +// +// ZX8081.hpp +// Clock Signal +// +// Created by Thomas Harte on 04/06/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#ifndef ZX8081_hpp +#define ZX8081_hpp + +#include "../ConfigurationTarget.hpp" +#include "../CRTMachine.hpp" + +#include "../../Processors/Z80/Z80.hpp" +#include "../../Storage/Tape/Tape.hpp" +#include "../../Storage/Tape/Parsers/ZX8081.hpp" + +#include "Video.hpp" + +#include +#include + +namespace ZX8081 { + +enum ROMType: uint8_t { + ZX80, ZX81 +}; + +enum Key: uint16_t { + KeyShift = 0x0000 | 0x01, KeyZ = 0x0000 | 0x02, KeyX = 0x0000 | 0x04, KeyC = 0x0000 | 0x08, KeyV = 0x0000 | 0x10, + KeyA = 0x0100 | 0x01, KeyS = 0x0100 | 0x02, KeyD = 0x0100 | 0x04, KeyF = 0x0100 | 0x08, KeyG = 0x0100 | 0x10, + KeyQ = 0x0200 | 0x01, KeyW = 0x0200 | 0x02, KeyE = 0x0200 | 0x04, KeyR = 0x0200 | 0x08, KeyT = 0x0200 | 0x10, + Key1 = 0x0300 | 0x01, Key2 = 0x0300 | 0x02, Key3 = 0x0300 | 0x04, Key4 = 0x0300 | 0x08, Key5 = 0x0300 | 0x10, + Key0 = 0x0400 | 0x01, Key9 = 0x0400 | 0x02, Key8 = 0x0400 | 0x04, Key7 = 0x0400 | 0x08, Key6 = 0x0400 | 0x10, + KeyP = 0x0500 | 0x01, KeyO = 0x0500 | 0x02, KeyI = 0x0500 | 0x04, KeyU = 0x0500 | 0x08, KeyY = 0x0500 | 0x10, + KeyEnter = 0x0600 | 0x01, KeyL = 0x0600 | 0x02, KeyK = 0x0600 | 0x04, KeyJ = 0x0600 | 0x08, KeyH = 0x0600 | 0x10, + KeySpace = 0x0700 | 0x01, KeyDot = 0x0700 | 0x02, KeyM = 0x0700 | 0x04, KeyN = 0x0700 | 0x08, KeyB = 0x0700 | 0x10, +}; + +class Machine: + public CPU::Z80::Processor, + public CRTMachine::Machine, + public ConfigurationTarget::Machine { + public: + Machine(); + + int perform_machine_cycle(const CPU::Z80::MachineCycle &cycle); + void flush(); + + void setup_output(float aspect_ratio); + void close_output(); + + std::shared_ptr get_crt(); + std::shared_ptr get_speaker(); + + void run_for_cycles(int number_of_cycles); + + void configure_as_target(const StaticAnalyser::Target &target); + + void set_rom(ROMType type, std::vector data); + void set_key_state(uint16_t key, bool isPressed); + void clear_all_keys(); + + private: + std::shared_ptr