From b7a62e012153dff9182c35a3a3a657ae2e9075f8 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 26 Apr 2021 20:47:28 -0400 Subject: [PATCH] Adds SZX support. Tweaking exposed Spectrum state object as relevant. --- Analyser/Static/StaticAnalyser.cpp | 2 + Components/AY38910/AY38910.hpp | 3 + Machines/Sinclair/ZXSpectrum/State.hpp | 2 - Machines/Sinclair/ZXSpectrum/Video.hpp | 41 +++- Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 2 - .../Clock Signal.xcodeproj/project.pbxproj | 8 + OSBindings/Mac/Clock Signal/Info.plist | 20 ++ Storage/State/SZX.cpp | 196 ++++++++++++++++++ Storage/State/SZX.hpp | 24 +++ Storage/State/Z80.cpp | 2 +- 10 files changed, 291 insertions(+), 9 deletions(-) create mode 100644 Storage/State/SZX.cpp create mode 100644 Storage/State/SZX.hpp diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp index 97160fe37..5a0a7996d 100644 --- a/Analyser/Static/StaticAnalyser.cpp +++ b/Analyser/Static/StaticAnalyser.cpp @@ -59,6 +59,7 @@ // State Snapshots #include "../../Storage/State/SNA.hpp" +#include "../../Storage/State/SZX.hpp" #include "../../Storage/State/Z80.hpp" // Tapes @@ -226,6 +227,7 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) { } Format("sna", SNA); + Format("szx", SZX); Format("z80", Z80); #undef TryInsert diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index cbd7e19b7..4ca99f6a2 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -198,12 +198,14 @@ struct Utility { struct State: public Reflection::StructImpl { uint8_t registers[16]{}; + uint8_t selected_register = 0; // TODO: all audio-production thread state. State() { if(needs_declare()) { DeclareField(registers); + DeclareField(selected_register); } } @@ -213,6 +215,7 @@ struct State: public Reflection::StructImpl { target.select_register(c); target.set_register_value(registers[c]); } + target.select_register(selected_register); } }; diff --git a/Machines/Sinclair/ZXSpectrum/State.hpp b/Machines/Sinclair/ZXSpectrum/State.hpp index 69d23c5f5..3d3613c19 100644 --- a/Machines/Sinclair/ZXSpectrum/State.hpp +++ b/Machines/Sinclair/ZXSpectrum/State.hpp @@ -30,7 +30,6 @@ struct State: public Reflection::StructImpl { // Meaningful for 128kb machines only. uint8_t last_7ffd = 0; - uint8_t last_fffd = 0; GI::AY38910::State ay; // Meaningful for the +2a and +3 only. @@ -42,7 +41,6 @@ struct State: public Reflection::StructImpl { DeclareField(video); DeclareField(ram); DeclareField(last_7ffd); - DeclareField(last_fffd); DeclareField(last_1ffd); DeclareField(ay); } diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp index 4bb96d883..75f0270d9 100644 --- a/Machines/Sinclair/ZXSpectrum/Video.hpp +++ b/Machines/Sinclair/ZXSpectrum/Video.hpp @@ -262,6 +262,39 @@ template class Video { } } + static constexpr HalfCycles frame_duration() { + const auto timings = get_timings(); + return HalfCycles(timings.cycles_per_line * timings.lines_per_frame); + } + + HalfCycles time_since_interrupt() { + const auto timings = get_timings(); + if(time_into_frame_ >= timings.interrupt_time) { + return HalfCycles(time_into_frame_ - timings.interrupt_time); + } else { + return HalfCycles(time_into_frame_) + frame_duration() - HalfCycles(timings.interrupt_time); + } + } + + void set_time_since_interrupt(const HalfCycles time) { + // Advance using run_for to ensure that all proper CRT interactions occurred. + const auto timings = get_timings(); + const auto target = (time + timings.interrupt_time) % frame_duration(); + const auto now = HalfCycles(time_into_frame_); + + // Maybe this is easy? + if(target == now) return; + + // Is the time within this frame? + if(time > now) { + run_for(target - time); + return; + } + + // Then it's necessary to finish this frame and run into the next. + run_for(frame_duration() - now + time); + } + public: Video() : crt_(half_cycles_per_line(), 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2) @@ -427,7 +460,7 @@ template class Video { struct State: public Reflection::StructImpl { uint8_t border_colour = 0; - int time_into_frame = 0; + int half_cycles_since_interrupt = 0; bool flash = 0; int flash_counter = 0; bool is_alternate_line = false; @@ -435,7 +468,7 @@ struct State: public Reflection::StructImpl { State() { if(needs_declare()) { DeclareField(border_colour); - DeclareField(time_into_frame); + DeclareField(half_cycles_since_interrupt); DeclareField(flash); DeclareField(flash_counter); DeclareField(is_alternate_line); @@ -444,18 +477,18 @@ struct State: public Reflection::StructImpl { template State(const Video &source) : State() { border_colour = source.border_byte_; - time_into_frame = source.time_into_frame_; flash = source.flash_mask_; flash_counter = source.flash_counter_; is_alternate_line = source. is_alternate_line_; + half_cycles_since_interrupt = source.time_since_interrupt().template as(); } template void apply(Video &target) { target.set_border_colour(border_colour); - target.time_into_frame_ = time_into_frame; target.flash_mask_ = flash ? 0xff : 0x00; target.flash_counter_ = flash_counter; target.is_alternate_line_ = is_alternate_line; + target.set_time_since_interrupt(HalfCycles(half_cycles_since_interrupt)); } }; diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp index e3c242d81..598708697 100644 --- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -143,8 +143,6 @@ template class ConcreteMachine: port1ffd_ = state->last_1ffd; port7ffd_ = state->last_7ffd; update_memory_map(); - - GI::AY38910::Utility::select_register(ay_, state->last_fffd); } } } diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 7d908b13d..a8acadf09 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -173,6 +173,8 @@ 4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53911D117D36003C6002 /* CSAudioQueue.m */; }; 4B2B3A4B1F9B8FA70062DABF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.cpp */; }; 4B2B3A4C1F9B8FA70062DABF /* MemoryFuzzer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A481F9B8FA70062DABF /* MemoryFuzzer.cpp */; }; + 4B2B946526377C0200E7097C /* SZX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B946326377C0200E7097C /* SZX.cpp */; }; + 4B2B946626377C0200E7097C /* SZX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B946326377C0200E7097C /* SZX.cpp */; }; 4B2BF19123DCC6A200C3AD60 /* BD500.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7BA03523CEB86000B98D9E /* BD500.cpp */; }; 4B2BF19223DCC6A800C3AD60 /* STX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7BA03323C58B1E00B98D9E /* STX.cpp */; }; 4B2BF19623E10F0100C3AD60 /* CSHighPrecisionTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BF19523E10F0000C3AD60 /* CSHighPrecisionTimer.m */; }; @@ -1142,6 +1144,8 @@ 4B2B3A481F9B8FA70062DABF /* MemoryFuzzer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MemoryFuzzer.cpp; sourceTree = ""; }; 4B2B3A491F9B8FA70062DABF /* MemoryFuzzer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MemoryFuzzer.hpp; sourceTree = ""; }; 4B2B3A4A1F9B8FA70062DABF /* Typer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Typer.hpp; sourceTree = ""; }; + 4B2B946326377C0200E7097C /* SZX.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SZX.cpp; sourceTree = ""; }; + 4B2B946426377C0200E7097C /* SZX.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SZX.hpp; sourceTree = ""; }; 4B2BF19423E10F0000C3AD60 /* CSHighPrecisionTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSHighPrecisionTimer.h; sourceTree = ""; }; 4B2BF19523E10F0000C3AD60 /* CSHighPrecisionTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSHighPrecisionTimer.m; sourceTree = ""; }; 4B2BFC5D1D613E0200BA3AA9 /* TapePRG.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TapePRG.cpp; sourceTree = ""; }; @@ -3263,6 +3267,8 @@ 4B8DD3832634D37E00B3C866 /* State */ = { isa = PBXGroup; children = ( + 4B2B946326377C0200E7097C /* SZX.cpp */, + 4B2B946426377C0200E7097C /* SZX.hpp */, 4B8DD3842634D37E00B3C866 /* SNA.cpp */, 4B8DD3852634D37E00B3C866 /* SNA.hpp */, 4B8DD39526360DDF00B3C866 /* Z80.cpp */, @@ -5237,6 +5243,7 @@ 4B1B58F7246CC4E8009C171E /* State.cpp in Sources */, 4B0ACC03237756F6008902D0 /* Line.cpp in Sources */, 4B055AB11FAE86070060FFFF /* Tape.cpp in Sources */, + 4B2B946626377C0200E7097C /* SZX.cpp in Sources */, 4BEDA43225B3C700000C2DBD /* Executor.cpp in Sources */, 4BC1317B2346DF2B00E4FF3D /* MSA.cpp in Sources */, 4B894533201967B4007DE474 /* 6502.cpp in Sources */, @@ -5531,6 +5538,7 @@ 4BEE0A701D72496600532C7B /* PRG.cpp in Sources */, 4BB307BB235001C300457D33 /* 6850.cpp in Sources */, 4BF437EE209D0F7E008CBD6B /* SegmentParser.cpp in Sources */, + 4B2B946526377C0200E7097C /* SZX.cpp in Sources */, 4BC131762346DE9100E4FF3D /* StaticAnalyser.cpp in Sources */, 4B8334861F5DA3780097E338 /* 6502Storage.cpp in Sources */, 4B8FE2271DA1DE2D0090D3CE /* NSBundle+DataResource.m in Sources */, diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist index b37475e53..c0ae89362 100644 --- a/OSBindings/Mac/Clock Signal/Info.plist +++ b/OSBindings/Mac/Clock Signal/Info.plist @@ -632,6 +632,26 @@ NSDocumentClass $(PRODUCT_MODULE_NAME).MachineDocument + + CFBundleTypeExtensions + + szx + + CFBundleTypeName + ZX Spectrum SZX snapshot + CFBundleTypeOSTypes + + ???? + + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSTypeIsPackage + + NSDocumentClass + $(PRODUCT_MODULE_NAME).MachineDocument + CFBundleExecutable $(EXECUTABLE_NAME) diff --git a/Storage/State/SZX.cpp b/Storage/State/SZX.cpp new file mode 100644 index 000000000..8a924f6ee --- /dev/null +++ b/Storage/State/SZX.cpp @@ -0,0 +1,196 @@ +// +// SZX.cpp +// Clock Signal +// +// Created by Thomas Harte on 26/04/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#include "SZX.hpp" + +#include "../FileHolder.hpp" + +#include "../../Analyser/Static/ZXSpectrum/Target.hpp" +#include "../../Machines/Sinclair/ZXSpectrum/State.hpp" + +#define LOG_PREFIX "[SZX] " +#include "../../Outputs/Log.hpp" + +#include + +using namespace Storage::State; + +std::unique_ptr SZX::load(const std::string &file_name) { + FileHolder file(file_name); + + // Construct a target with a Spectrum state. + using Target = Analyser::Static::ZXSpectrum::Target; + auto result = std::make_unique(); + auto *const state = new Sinclair::ZXSpectrum::State(); + result->state = std::unique_ptr(state); + + // Check signature and major version number. + if(!file.check_signature("ZXST")) { + return nullptr; + } + const uint8_t major_version = file.get8(); + [[maybe_unused]] const uint8_t minor_version = file.get8(); + if(major_version > 1) { + return nullptr; + } + + // Check for a supported machine type. + const uint8_t machine_type = file.get8(); + switch(machine_type) { + default: return nullptr; + + case 0: result->model = Target::Model::SixteenK; break; + case 1: result->model = Target::Model::FortyEightK; break; + case 2: result->model = Target::Model::OneTwoEightK; break; + case 3: result->model = Target::Model::Plus2; break; + case 4: result->model = Target::Model::Plus2a; break; + case 5: result->model = Target::Model::Plus3; break; + } + + // Consequential upon selected machine... + switch(result->model) { + case Target::Model::SixteenK: state->ram.resize(16 * 1024); break; + case Target::Model::FortyEightK: state->ram.resize(48 * 1024); break; + default: + state->ram.resize(128 * 1024); + break; + } + + const uint8_t file_flags = file.get8(); + [[maybe_unused]] const bool uses_late_timings = file_flags & 1; + + // Now parse all included blocks. + while(true) { + const uint32_t blockID = file.get32le(); + const uint32_t size = file.get32le(); + if(file.eof()) break; + const auto location = file.tell(); + +#define BLOCK(str) str[0] | (str[1] << 8) | (str[2] << 16) | (str[3] << 24) + + switch(blockID) { + default: + LOG("Unhandled block " << char(blockID) << char(blockID >> 8) << char(blockID >> 16) << char(blockID >> 24)); + break; + + // ZXSTZ80REGS + case BLOCK("Z80R"): { + state->z80.registers.flags = file.get8(); + state->z80.registers.a = file.get8(); + + state->z80.registers.bc = file.get16le(); + state->z80.registers.de = file.get16le(); + state->z80.registers.hl = file.get16le(); + + state->z80.registers.af_dash = file.get16le(); + state->z80.registers.bc_dash = file.get16le(); + state->z80.registers.de_dash = file.get16le(); + state->z80.registers.hl_dash = file.get16le(); + + state->z80.registers.ix = file.get16le(); + state->z80.registers.iy = file.get16le(); + state->z80.registers.stack_pointer = file.get16le(); + state->z80.registers.program_counter = file.get16le(); + + const uint8_t i = file.get8(); + const uint8_t r = file.get8(); + state->z80.registers.ir = uint16_t((i << 8) | r); + + state->z80.registers.iff1 = file.get8(); + state->z80.registers.iff2 = file.get8(); + state->z80.registers.interrupt_mode = file.get8(); + + state->video.half_cycles_since_interrupt = int(file.get32le()) * 2; + + // SZX includes a count of remaining cycles that interrupt should be asserted for + // because it supports hardware that might cause an interrupt other than the display. + // This emulator doesn't, so this field can be ignored. + [[maybe_unused]] uint8_t remaining_interrupt_cycles = file.get8(); + + + const uint8_t flags = file.get8(); + state->z80.execution_state.is_halted = flags & 2; + // TODO: bit 0 indicates that the last instruction was an EI, or an invalid + // DD or FD. I assume I'm supposed to use that to conclude an interrupt + // verdict but I'm unclear what the effect of an invalid DD or FD is so + // have not yet implemented this. + + state->z80.registers.memptr = file.get16le(); + } break; + + // ZXSTAYBLOCK + case BLOCK("AY\0\0"): { + // This applies to 48kb machines with AY boxes only. This emulator + // doesn't currently support those. + [[maybe_unused]] const uint8_t interface_type = file.get8(); + + state->ay.selected_register = file.get8(); + file.read(state->ay.registers, 16); + } break; + + // ZXSTRAMPAGE + case BLOCK("RAMP"): { + const uint16_t flags = file.get16le(); + const uint8_t page = file.get8(); + + std::vector contents; + if(flags & 1) { + // ZLib compression is applied. + contents.resize(16 * 1024); + const std::vector source = file.read(size - 3); + + uLongf output_length; + uncompress(contents.data(), &output_length, source.data(), source.size()); + assert(output_length == contents.size()); + } else { + // Data is raw. + contents = file.read(16 * 1024); + } + + switch(result->model) { + case Target::Model::SixteenK: + case Target::Model::FortyEightK: { + size_t address = 0; + switch(page) { + default: break; + case 5: address = 0x4000; break; + case 2: address = 0x8000; break; + case 0: address = 0xc000; break; + } + + if(address > 0 && (address - 0x4000) <= state->ram.size()) { + memcpy(&state->ram[address - 0x4000], contents.data(), 0x4000); + } + } break; + + default: + if(page < 8) { + memcpy(&state->ram[page * 0x4000], contents.data(), 0x4000); + } + break; + } + } break; + + // ZXSTSPECREGS + case BLOCK("SPCR"): { + state->video.border_colour = file.get8(); + state->last_7ffd = file.get8(); + state->last_1ffd = file.get8(); + + // TODO: use last write to FE, at least. + } break; + } + +#undef BLOCK + + // Advance to the next block. + file.seek(location + size, SEEK_SET); + } + + return result; +} diff --git a/Storage/State/SZX.hpp b/Storage/State/SZX.hpp new file mode 100644 index 000000000..56d8b0146 --- /dev/null +++ b/Storage/State/SZX.hpp @@ -0,0 +1,24 @@ +// +// SZX.hpp +// Clock Signal +// +// Created by Thomas Harte on 26/04/2021. +// Copyright © 2021 Thomas Harte. All rights reserved. +// + +#ifndef Storage_State_SZX_hpp +#define Storage_State_SZX_hpp + +#include "../../Analyser/Static/StaticAnalyser.hpp" + +namespace Storage { +namespace State { + +struct SZX { + static std::unique_ptr load(const std::string &file_name); +}; + +} +} + +#endif /* Storage_State_SZX_hpp */ diff --git a/Storage/State/Z80.cpp b/Storage/State/Z80.cpp index 4f5c9d390..2fa40135b 100644 --- a/Storage/State/Z80.cpp +++ b/Storage/State/Z80.cpp @@ -141,7 +141,7 @@ std::unique_ptr Z80::load(const std::string &file_name } } - state->last_fffd = file.get8(); + state->ay.selected_register = file.get8(); file.read(state->ay.registers, 16); if(bonus_header_size != 23) {