mirror of
https://github.com/TomHarte/CLK.git
synced 2024-12-22 14:30:29 +00:00
Adds SZX support.
Tweaking exposed Spectrum state object as relevant.
This commit is contained in:
parent
bd5dd9b9a3
commit
b7a62e0121
@ -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
|
||||
|
@ -198,12 +198,14 @@ struct Utility {
|
||||
|
||||
struct State: public Reflection::StructImpl<State> {
|
||||
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<State> {
|
||||
target.select_register(c);
|
||||
target.set_register_value(registers[c]);
|
||||
}
|
||||
target.select_register(selected_register);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -30,7 +30,6 @@ struct State: public Reflection::StructImpl<State> {
|
||||
|
||||
// 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<State> {
|
||||
DeclareField(video);
|
||||
DeclareField(ram);
|
||||
DeclareField(last_7ffd);
|
||||
DeclareField(last_fffd);
|
||||
DeclareField(last_1ffd);
|
||||
DeclareField(ay);
|
||||
}
|
||||
|
@ -262,6 +262,39 @@ template <Timing timing> 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 <Timing timing> class Video {
|
||||
|
||||
struct State: public Reflection::StructImpl<State> {
|
||||
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> {
|
||||
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<State> {
|
||||
|
||||
template <typename Video> 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<int>();
|
||||
}
|
||||
|
||||
template <typename Video> 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));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -143,8 +143,6 @@ template<Model model> class ConcreteMachine:
|
||||
port1ffd_ = state->last_1ffd;
|
||||
port7ffd_ = state->last_7ffd;
|
||||
update_memory_map();
|
||||
|
||||
GI::AY38910::Utility::select_register(ay_, state->last_fffd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 = "<group>"; };
|
||||
4B2B3A491F9B8FA70062DABF /* MemoryFuzzer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MemoryFuzzer.hpp; sourceTree = "<group>"; };
|
||||
4B2B3A4A1F9B8FA70062DABF /* Typer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Typer.hpp; sourceTree = "<group>"; };
|
||||
4B2B946326377C0200E7097C /* SZX.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SZX.cpp; sourceTree = "<group>"; };
|
||||
4B2B946426377C0200E7097C /* SZX.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SZX.hpp; sourceTree = "<group>"; };
|
||||
4B2BF19423E10F0000C3AD60 /* CSHighPrecisionTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSHighPrecisionTimer.h; sourceTree = "<group>"; };
|
||||
4B2BF19523E10F0000C3AD60 /* CSHighPrecisionTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSHighPrecisionTimer.m; sourceTree = "<group>"; };
|
||||
4B2BFC5D1D613E0200BA3AA9 /* TapePRG.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TapePRG.cpp; sourceTree = "<group>"; };
|
||||
@ -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 */,
|
||||
|
@ -632,6 +632,26 @@
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>szx</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ZX Spectrum SZX snapshot</string>
|
||||
<key>CFBundleTypeOSTypes</key>
|
||||
<array>
|
||||
<string>????</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<false/>
|
||||
<key>NSDocumentClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
|
196
Storage/State/SZX.cpp
Normal file
196
Storage/State/SZX.cpp
Normal file
@ -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 <zlib.h>
|
||||
|
||||
using namespace Storage::State;
|
||||
|
||||
std::unique_ptr<Analyser::Static::Target> 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<Target>();
|
||||
auto *const state = new Sinclair::ZXSpectrum::State();
|
||||
result->state = std::unique_ptr<Reflection::Struct>(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<uint8_t> contents;
|
||||
if(flags & 1) {
|
||||
// ZLib compression is applied.
|
||||
contents.resize(16 * 1024);
|
||||
const std::vector<uint8_t> 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;
|
||||
}
|
24
Storage/State/SZX.hpp
Normal file
24
Storage/State/SZX.hpp
Normal file
@ -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<Analyser::Static::Target> load(const std::string &file_name);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* Storage_State_SZX_hpp */
|
@ -141,7 +141,7 @@ std::unique_ptr<Analyser::Static::Target> 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) {
|
||||
|
Loading…
Reference in New Issue
Block a user