1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-04 18:29:40 +00:00

Merge pull request #844 from TomHarte/AppleIIgs

Adds incomplete Apple IIgs emulation.
This commit is contained in:
Thomas Harte 2021-03-06 21:27:39 -05:00 committed by GitHub
commit 63a792f434
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
156 changed files with 458527 additions and 1376 deletions

View File

@ -14,6 +14,7 @@ namespace Analyser {
enum class Machine {
AmstradCPC,
AppleII,
AppleIIgs,
Atari2600,
AtariST,
ColecoVision,

View File

@ -6,8 +6,8 @@
// Copyright 2018 Thomas Harte. All rights reserved.
//
#ifndef Target_h
#define Target_h
#ifndef Analyser_Static_AppleII_Target_h
#define Analyser_Static_AppleII_Target_h
#include "../../../Reflection/Enum.hpp"
#include "../../../Reflection/Struct.hpp"
@ -47,4 +47,4 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Ta
}
}
#endif /* Target_h */
#endif /* Analyser_Static_AppleII_Target_h */

View File

@ -0,0 +1,19 @@
//
// StaticAnalyser.cpp
// Clock Signal
//
// Created by Thomas Harte on 20/10/2020.
// Copyright 2018 Thomas Harte. All rights reserved.
//
#include "StaticAnalyser.hpp"
#include "Target.hpp"
Analyser::Static::TargetList Analyser::Static::AppleIIgs::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) {
auto target = std::make_unique<Target>();
target->media = media;
TargetList targets;
targets.push_back(std::move(target));
return targets;
}

View File

@ -0,0 +1,26 @@
//
// StaticAnalyser.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/10/2020.
// Copyright 2018 Thomas Harte. All rights reserved.
//
#ifndef Analyser_Static_AppleIIgs_StaticAnalyser_hpp
#define Analyser_Static_AppleIIgs_StaticAnalyser_hpp
#include "../StaticAnalyser.hpp"
#include "../../../Storage/TargetPlatforms.hpp"
#include <string>
namespace Analyser {
namespace Static {
namespace AppleIIgs {
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
}
}
}
#endif /* Analyser_Static_AppleIIgs_StaticAnalyser_hpp */

View File

@ -0,0 +1,49 @@
//
// Target.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/10/2020.
// Copyright 2018 Thomas Harte. All rights reserved.
//
#ifndef Analyser_Static_AppleIIgs_Target_h
#define Analyser_Static_AppleIIgs_Target_h
#include "../../../Reflection/Enum.hpp"
#include "../../../Reflection/Struct.hpp"
#include "../StaticAnalyser.hpp"
namespace Analyser {
namespace Static {
namespace AppleIIgs {
struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Target> {
ReflectableEnum(Model,
ROM00,
ROM01,
ROM03
);
ReflectableEnum(MemoryModel,
TwoHundredAndFiftySixKB,
OneMB,
EightMB
);
Model model = Model::ROM03;
MemoryModel memory_model = MemoryModel::EightMB;
Target() : Analyser::Static::Target(Machine::AppleIIgs) {
if(needs_declare()) {
DeclareField(model);
DeclareField(memory_model);
AnnounceEnum(Model);
AnnounceEnum(MemoryModel);
}
}
};
}
}
}
#endif /* Analyser_Static_AppleIIgs_Target_h */

View File

@ -9,6 +9,7 @@
#include "StaticAnalyser.hpp"
#include "../AppleII/Target.hpp"
#include "../AppleIIgs/Target.hpp"
#include "../Oric/Target.hpp"
#include "../Disassembler/6502.hpp"
#include "../Disassembler/AddressMapper.hpp"
@ -18,7 +19,7 @@
namespace {
Analyser::Static::Target *AppleTarget(const Storage::Encodings::AppleGCR::Sector *sector_zero) {
Analyser::Static::Target *AppleIITarget(const Storage::Encodings::AppleGCR::Sector *sector_zero) {
using Target = Analyser::Static::AppleII::Target;
auto *const target = new Target;
@ -31,6 +32,10 @@ Analyser::Static::Target *AppleTarget(const Storage::Encodings::AppleGCR::Sector
return target;
}
Analyser::Static::Target *AppleIIgsTarget() {
return new Analyser::Static::AppleIIgs::Target();
}
Analyser::Static::Target *OricTarget(const Storage::Encodings::AppleGCR::Sector *) {
using Target = Analyser::Static::Oric::Target;
auto *const target = new Target;
@ -46,8 +51,18 @@ Analyser::Static::TargetList Analyser::Static::DiskII::GetTargets(const Media &m
// This analyser can comprehend disks only.
if(media.disks.empty()) return {};
auto &disk = media.disks.front();
TargetList targets;
// If the disk image is too large for a 5.25" disk, map this to the IIgs.
if(disk->get_maximum_head_position() > Storage::Disk::HeadPosition(40)) {
targets.push_back(std::unique_ptr<Analyser::Static::Target>(AppleIIgsTarget()));
targets.back()->media = media;
return targets;
}
// Grab track 0, sector 0: the boot sector.
const auto track_zero = media.disks.front()->get_track_at_position(Storage::Disk::Track::Address(0, Storage::Disk::HeadPosition(0)));
const auto track_zero = disk->get_track_at_position(Storage::Disk::Track::Address(0, Storage::Disk::HeadPosition(0)));
const auto sector_map = Storage::Encodings::AppleGCR::sectors_from_segment(
Storage::Disk::track_serialisation(*track_zero, Storage::Time(1, 50000)));
@ -61,12 +76,11 @@ Analyser::Static::TargetList Analyser::Static::DiskII::GetTargets(const Media &m
// If there's no boot sector then if there are also no sectors at all,
// decline to nominate a machine. Otherwise go with an Apple as the default.
TargetList targets;
if(!sector_zero) {
if(sector_map.empty()) {
return targets;
} else {
targets.push_back(std::unique_ptr<Analyser::Static::Target>(AppleTarget(nullptr)));
targets.push_back(std::unique_ptr<Analyser::Static::Target>(AppleIITarget(nullptr)));
targets.back()->media = media;
return targets;
}
@ -116,7 +130,7 @@ Analyser::Static::TargetList Analyser::Static::DiskII::GetTargets(const Media &m
if(is_oric) {
targets.push_back(std::unique_ptr<Analyser::Static::Target>(OricTarget(sector_zero)));
} else {
targets.push_back(std::unique_ptr<Analyser::Static::Target>(AppleTarget(sector_zero)));
targets.push_back(std::unique_ptr<Analyser::Static::Target>(AppleIITarget(sector_zero)));
}
targets.back()->media = media;
return targets;

View File

@ -17,6 +17,7 @@
#include "Acorn/StaticAnalyser.hpp"
#include "AmstradCPC/StaticAnalyser.hpp"
#include "AppleII/StaticAnalyser.hpp"
#include "AppleIIgs/StaticAnalyser.hpp"
#include "Atari2600/StaticAnalyser.hpp"
#include "AtariST/StaticAnalyser.hpp"
#include "Coleco/StaticAnalyser.hpp"
@ -33,6 +34,7 @@
#include "../../Storage/Cartridge/Formats/PRG.hpp"
// Disks
#include "../../Storage/Disk/DiskImage/Formats/2MG.hpp"
#include "../../Storage/Disk/DiskImage/Formats/AcornADF.hpp"
#include "../../Storage/Disk/DiskImage/Formats/AppleDSK.hpp"
#include "../../Storage/Disk/DiskImage/Formats/CPCDSK.hpp"
@ -79,20 +81,31 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
std::string extension = file_name.substr(final_dot + 1);
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
#define Insert(list, class, platforms) \
list.emplace_back(new Storage::class(file_name));\
#define InsertInstance(list, instance, platforms) \
list.emplace_back(instance);\
potential_platforms |= platforms;\
TargetPlatform::TypeDistinguisher *distinguisher = dynamic_cast<TargetPlatform::TypeDistinguisher *>(list.back().get());\
if(distinguisher) potential_platforms &= distinguisher->target_platform_type();
if(distinguisher) potential_platforms &= distinguisher->target_platform_type(); \
#define TryInsert(list, class, platforms) \
#define Insert(list, class, platforms, ...) \
InsertInstance(list, new Storage::class(__VA_ARGS__), platforms);
#define TryInsert(list, class, platforms, ...) \
try {\
Insert(list, class, platforms) \
Insert(list, class, platforms, __VA_ARGS__) \
} catch(...) {}
#define Format(ext, list, class, platforms) \
if(extension == ext) { \
TryInsert(list, class, platforms) \
TryInsert(list, class, platforms, file_name) \
}
// 2MG
if(extension == "2mg") {
// 2MG uses a factory method; defer to it.
try {
InsertInstance(result.disks, Storage::Disk::Disk2MG::open(file_name), TargetPlatform::DiskII)
} catch(...) {}
}
Format("80", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 80
@ -131,17 +144,23 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII) // NIB
Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // O
Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P
Format("po", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // PO
Format("po", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // PO (original Apple II kind)
// PO (Apple IIgs kind)
if(extension == "po") {
TryInsert(result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::AppleIIgs, file_name, Storage::Disk::MacintoshIMG::FixedType::GCR)
}
Format("p81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P81
// PRG
if(extension == "prg") {
// try instantiating as a ROM; failing that accept as a tape
try {
Insert(result.cartridges, Cartridge::PRG, TargetPlatform::Commodore)
Insert(result.cartridges, Cartridge::PRG, TargetPlatform::Commodore, file_name)
} catch(...) {
try {
Insert(result.tapes, Tape::PRG, TargetPlatform::Commodore)
Insert(result.tapes, Tape::PRG, TargetPlatform::Commodore, file_name)
} catch(...) {}
}
}
@ -165,6 +184,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
#undef Format
#undef Insert
#undef TryInsert
#undef InsertInstance
return result;
}
@ -191,6 +211,7 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) {
if(potential_platforms & TargetPlatform::Acorn) Append(Acorn);
if(potential_platforms & TargetPlatform::AmstradCPC) Append(AmstradCPC);
if(potential_platforms & TargetPlatform::AppleII) Append(AppleII);
if(potential_platforms & TargetPlatform::AppleIIgs) Append(AppleIIgs);
if(potential_platforms & TargetPlatform::Atari2600) Append(Atari2600);
if(potential_platforms & TargetPlatform::AtariST) Append(AtariST);
if(potential_platforms & TargetPlatform::ColecoVision) Append(Coleco);

View File

@ -21,44 +21,86 @@
Machines that accumulate HalfCycle time but supply to a Cycle-counted device may supply a
separate @c TargetTimeScale at template declaration.
If the held object implements get_next_sequence_point() then it'll be used to flush implicitly
as and when sequence points are hit. Callers can use will_flush() to predict these.
*/
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class JustInTimeActor {
private:
class SequencePointAwareDeleter {
public:
explicit SequencePointAwareDeleter(JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *actor) : actor_(actor) {}
void operator ()(const T *const) const {
if constexpr (has_sequence_points<T>::value) {
actor_->update_sequence_point();
}
}
private:
JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *const actor_;
};
public:
/// Constructs a new JustInTimeActor using the same construction arguments as the included object.
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
/// Adds time to the actor.
forceinline void operator += (const LocalTimeScale &rhs) {
forceinline void operator += (LocalTimeScale rhs) {
if constexpr (multiplier != 1) {
time_since_update_ += rhs * multiplier;
} else {
time_since_update_ += rhs;
}
is_flushed_ = false;
if constexpr (has_sequence_points<T>::value) {
time_until_event_ -= rhs;
if(time_until_event_ <= LocalTimeScale(0)) {
flush();
update_sequence_point();
}
}
}
/// Flushes all accumulated time and returns a pointer to the included object.
forceinline T *operator->() {
///
/// If this object provides sequence points, checks for changes to the next
/// sequence point upon deletion of the pointer.
forceinline auto operator->() {
flush();
return &object_;
return std::unique_ptr<T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(this));
}
/// Acts exactly as per the standard ->, but preserves constness.
forceinline const T *operator->() const {
///
/// Despite being const, this will flush the object and, if relevant, update the next sequence point.
forceinline auto operator -> () const {
auto non_const_this = const_cast<JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *>(this);
non_const_this->flush();
return &object_;
return std::unique_ptr<const T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(non_const_this));
}
/// Returns a pointer to the included object without flushing time.
/// @returns a pointer to the included object, without flushing time.
forceinline T *last_valid() {
return &object_;
}
/// @returns the amount of time since the object was last flushed, in the target time scale.
forceinline TargetTimeScale time_since_flush() const {
// TODO: does this handle conversions properly where TargetTimeScale != LocalTimeScale?
if constexpr (divider == 1) {
return time_since_update_;
}
return TargetTimeScale(time_since_update_.as_integral() / divider);
}
/// Flushes all accumulated time.
///
/// This does not affect this actor's record of when the next sequence point will occur.
forceinline void flush() {
if(!is_flushed_) {
is_flushed_ = true;
did_flush_ = is_flushed_ = true;
if constexpr (divider == 1) {
const auto duration = time_since_update_.template flush<TargetTimeScale>();
object_.run_for(duration);
@ -70,10 +112,43 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
}
}
/// Indicates whether a flush has occurred since the last call to did_flush().
forceinline bool did_flush() {
const bool did_flush = did_flush_;
did_flush_ = false;
return did_flush;
}
/// @returns the number of cycles until the next sequence-point-based flush, if the embedded object
/// supports sequence points; @c LocalTimeScale() otherwise.
LocalTimeScale cycles_until_implicit_flush() const {
return time_until_event_;
}
/// Indicates whether a sequence-point-caused flush will occur if the specified period is added.
forceinline bool will_flush(LocalTimeScale rhs) const {
if constexpr (!has_sequence_points<T>::value) {
return false;
}
return rhs >= time_until_event_;
}
/// Updates this template's record of the next sequence point.
void update_sequence_point() {
if constexpr (has_sequence_points<T>::value) {
time_until_event_ = object_.get_next_sequence_point();
assert(time_until_event_ > LocalTimeScale(0));
}
}
private:
T object_;
LocalTimeScale time_since_update_;
LocalTimeScale time_since_update_, time_until_event_;
bool is_flushed_ = true;
bool did_flush_ = false;
template <typename S, typename = void> struct has_sequence_points : std::false_type {};
template <typename S> struct has_sequence_points<S, decltype(void(std::declval<S &>().get_next_sequence_point()))> : std::true_type {};
};
/*!

View File

@ -8,6 +8,7 @@
#include "z8530.hpp"
#define NDEBUG
#define LOG_PREFIX "[SCC] "
#include "../../Outputs/Log.hpp"

View File

@ -0,0 +1,290 @@
//
// RealTimeClock.hpp
// Clock Signal
//
// Created by Thomas Harte on 07/05/2019.
// Copyright © 2019 Thomas Harte. All rights reserved.
//
#ifndef Apple_RealTimeClock_hpp
#define Apple_RealTimeClock_hpp
namespace Apple {
namespace Clock {
/*!
Models Apple's real-time clocks, as contained in the Macintosh and IIgs.
Since tracking of time is pushed to this class, it is assumed
that whomever is translating real time into emulated time
will also signal interrupts this is just the storage and time counting.
*/
class ClockStorage {
public:
ClockStorage() {
// TODO: this should persist, if possible, rather than
// being default initialised.
constexpr uint8_t default_data[] = {
0xa8, 0x00, 0x00, 0x00,
0xcc, 0x0a, 0xcc, 0x0a,
0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0x63, 0x00,
0x03, 0x88, 0x00, 0x4c
};
memcpy(data_, default_data, sizeof(default_data));
memset(&data_[sizeof(default_data)], 0xff, sizeof(data_) - sizeof(default_data));
}
/*!
Advances the clock by 1 second.
The caller should also signal an interrupt.
*/
void update() {
for(int c = 0; c < 4; ++c) {
++seconds_[c];
if(seconds_[c]) break;
}
}
protected:
static constexpr uint16_t NoResult = 0x100;
static constexpr uint16_t DidComplete = 0x101;
uint16_t perform(uint8_t command) {
/*
Documented commands:
z0000001 Seconds register 0 (lowest order byte)
z0000101 Seconds register 1
z0001001 Seconds register 2
z0001101 Seconds register 3
00110001 Test register (write only)
00110101 Write-protect register (write only)
z010aa01 RAM addresses 0x10 - 0x13
z1aaaa01 RAM addresses 0x00 0x0f
z0111abc, followed by 0defgh00
RAM address abcdefgh
z = 1 => a read; z = 0 => a write.
The top bit of the write-protect register enables (0) or disables (1)
writes to other locations.
All the documentation says about the test register is to set the top
two bits to 0 for normal operation. Abnormal operation is undefined.
*/
switch(phase_) {
case Phase::Command:
// Decode an address.
switch(command & 0x70) {
default:
if(command & 0x40) {
// RAM addresses 0x00 0x0f.
address_ = (command >> 2) & 0xf;
} else return DidComplete; // Unrecognised.
break;
case 0x00:
// A time access.
address_ = SecondsBuffer + ((command >> 2)&3);
break;
case 0x30:
// Either a register access or an extended instruction.
if(command & 0x08) {
address_ = (command & 0x7) << 5;
phase_ = (command & 0x80) ? Phase::SecondAddressByteRead : Phase::SecondAddressByteWrite;
return NoResult;
} else {
address_ = (command & 4) ? RegisterWriteProtect : RegisterTest;
}
break;
case 0x20:
// RAM addresses 0x10 0x13.
address_ = 0x10 + ((command >> 2) & 0x3);
break;
}
// If this is a read, return a result; otherwise prepare to write.
if(command & 0x80) {
// The two registers are write-only.
if(address_ == RegisterTest || address_ == RegisterWriteProtect) {
return DidComplete;
}
return (address_ >= SecondsBuffer) ? seconds_[address_ & 0xff] : data_[address_];
}
phase_ = Phase::WriteData;
return NoResult;
case Phase::SecondAddressByteRead:
case Phase::SecondAddressByteWrite:
if(command & 0x83) {
return DidComplete;
}
address_ |= command >> 2;
if(phase_ == Phase::SecondAddressByteRead) {
phase_ = Phase::Command;
return data_[address_]; // Only RAM accesses can get this far.
} else {
phase_ = Phase::WriteData;
}
return NoResult;
case Phase::WriteData:
// First test: is this to the write-protect register?
if(address_ == RegisterWriteProtect) {
write_protect_ = command;
return DidComplete;
}
if(address_ == RegisterTest) {
// No documentation here.
return DidComplete;
}
// No other writing is permitted if the write protect
// register won't allow it.
if(!(write_protect_ & 0x80)) {
if(address_ >= SecondsBuffer) {
seconds_[address_ & 0xff] = command;
} else {
data_[address_] = command;
}
}
phase_ = Phase::Command;
return DidComplete;
}
return NoResult;
}
private:
uint8_t data_[256];
uint8_t seconds_[4];
uint8_t write_protect_;
int address_;
static constexpr int SecondsBuffer = 0x100;
static constexpr int RegisterTest = 0x200;
static constexpr int RegisterWriteProtect = 0x201;
enum class Phase {
Command,
SecondAddressByteRead,
SecondAddressByteWrite,
WriteData
};
Phase phase_ = Phase::Command;
};
/*!
Provides the serial interface implemented by the Macintosh.
*/
class SerialClock: public ClockStorage {
public:
/*!
Sets the current clock and data inputs to the clock.
*/
void set_input(bool clock, bool data) {
// The data line is valid when the clock transitions to level 0.
if(clock && !previous_clock_) {
// Shift into the command_ register, no matter what.
command_ = uint16_t((command_ << 1) | (data ? 1 : 0));
result_ <<= 1;
// Increment phase.
++phase_;
// If a whole byte has been collected, push it onwards.
if(!(phase_&7)) {
// Begin pessimistically.
const auto effect = perform(uint8_t(command_));
switch(effect) {
case ClockStorage::NoResult:
break;
default:
result_ = uint8_t(effect);
break;
case ClockStorage::DidComplete:
abort();
break;
}
}
}
previous_clock_ = clock;
}
/*!
Reads the current data output level from the clock.
*/
bool get_data() {
return !!(result_ & 0x80);
}
/*!
Announces that a serial command has been aborted.
*/
void abort() {
result_ = 0;
phase_ = 0;
command_ = 0;
}
private:
int phase_ = 0;
uint16_t command_;
uint8_t result_ = 0;
bool previous_clock_ = false;
};
/*!
Provides the parallel interface implemented by the IIgs.
*/
class ParallelClock: public ClockStorage {
public:
void set_control(uint8_t control) {
if(!(control&0x80)) return;
if(control & 0x40) {
// Read from the RTC.
// A no-op for now.
} else {
// Write to the RTC. Which in this implementation also sets up a future read.
data_ = uint8_t(perform(data_));
}
// MAGIC! The transaction took 0 seconds.
// TODO: no magic.
control_ = control & 0x7f;
// Bit 5 is also meant to be 1 or 0 to indicate the final byte.
}
uint8_t get_control() {
return control_;
}
void set_data(uint8_t data) {
data_ = data;
}
uint8_t get_data() {
return data_;
}
private:
uint8_t data_;
uint8_t control_;
};
}
}
#endif /* Apple_RealTimeClock_hpp */

View File

@ -0,0 +1,45 @@
//
// DiskIIDrive.cpp
// Clock Signal
//
// Created by Thomas Harte on 20/11/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#include "DiskIIDrive.hpp"
using namespace Apple::Disk;
DiskIIDrive::DiskIIDrive(int input_clock_rate) :
IWMDrive(input_clock_rate, 1) {
Drive::set_rotation_speed(300.0f);
}
void DiskIIDrive::set_enabled(bool enabled) {
set_motor_on(enabled);
}
void DiskIIDrive::set_control_lines(int lines) {
// If the stepper magnet selections have changed, and any is on, see how
// that moves the head.
if(lines ^ stepper_mask_ && lines) {
// Convert from a representation of bits set to the centre of pull.
int direction = 0;
if(lines&1) direction += (((stepper_position_ - 0) + 4)&7) - 4;
if(lines&2) direction += (((stepper_position_ - 2) + 4)&7) - 4;
if(lines&4) direction += (((stepper_position_ - 4) + 4)&7) - 4;
if(lines&8) direction += (((stepper_position_ - 6) + 4)&7) - 4;
const int bits_set = (lines&1) + ((lines >> 1)&1) + ((lines >> 2)&1) + ((lines >> 3)&1);
direction /= bits_set;
// Compare to the stepper position to decide whether that pulls in the
// current cog notch, or grabs a later one.
step(Storage::Disk::HeadPosition(-direction, 4));
stepper_position_ = (stepper_position_ - direction + 8) & 7;
}
stepper_mask_ = lines;
}
bool DiskIIDrive::read() {
return !!(stepper_mask_ & 2) || get_is_read_only();
}

View File

@ -0,0 +1,33 @@
//
// DiskIIDrive.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/11/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef DiskIIDrive_hpp
#define DiskIIDrive_hpp
#include "IWM.hpp"
namespace Apple {
namespace Disk {
class DiskIIDrive: public IWMDrive {
public:
DiskIIDrive(int input_clock_rate);
private:
void set_enabled(bool) final;
void set_control_lines(int) final;
bool read() final;
int stepper_mask_ = 0;
int stepper_position_ = 0;
};
}
}
#endif /* DiskIIDrive_hpp */

View File

@ -8,6 +8,8 @@
#include "IWM.hpp"
#define NDEBUG
#define LOG_PREFIX "[IWM] "
#include "../../Outputs/Log.hpp"
using namespace Apple;
@ -50,7 +52,7 @@ uint8_t IWM::read(int address) {
switch(state_ & (Q6 | Q7 | ENABLE)) {
default:
LOG("[IWM] Invalid read\n");
LOG("Invalid read\n");
return 0xff;
// "Read all 1s".
@ -62,9 +64,8 @@ uint8_t IWM::read(int address) {
const auto result = data_register_;
if(data_register_ & 0x80) {
// printf("\n\nIWM:%02x\n\n", data_register_);
// printf(".");
data_register_ = 0;
// LOG("Reading data: " << PADHEX(2) << int(result));
}
// LOG("Reading data register: " << PADHEX(2) << int(result));
@ -99,7 +100,7 @@ uint8_t IWM::read(int address) {
bit 6: 1 = write state (0 = underrun has occurred; 1 = no underrun so far).
bit 7: 1 = write data buffer ready for data (1 = ready; 0 = busy).
*/
// LOG("Reading write handshake: " << PADHEX(2) << (0x3f | write_handshake_));
// LOG("Reading write handshake: " << PADHEX(2) << int(0x3f | write_handshake_));
return 0x3f | write_handshake_;
}
@ -128,13 +129,21 @@ void IWM::write(int address, uint8_t input) {
mode_ = input;
// TEMPORARY. To test for the unimplemented mode.
if(input&0x2) {
LOG("Switched to asynchronous mode");
} else {
LOG("Switched to synchronous mode");
}
switch(mode_ & 0x18) {
case 0x00: bit_length_ = Cycles(24); break; // slow mode, 7Mhz
case 0x08: bit_length_ = Cycles(12); break; // fast mode, 7Mhz
case 0x00: bit_length_ = Cycles(28); break; // slow mode, 7Mhz
case 0x08: bit_length_ = Cycles(14); break; // fast mode, 7Mhz
case 0x10: bit_length_ = Cycles(32); break; // slow mode, 8Mhz
case 0x18: bit_length_ = Cycles(16); break; // fast mode, 8Mhz
}
LOG("IWM mode is now " << PADHEX(2) << int(mode_));
LOG("Mode is now " << PADHEX(2) << int(mode_));
LOG("New bit length is " << std::dec << bit_length_.as_integral());
break;
case Q7|Q6|ENABLE: // Write data register.
@ -248,6 +257,7 @@ void IWM::run_for(const Cycles cycles) {
drives_[active_drive_]->run_for(Cycles(1));
++cycles_since_shift_;
if(cycles_since_shift_ == bit_length_ + error_margin) {
// LOG("Shifting 0 at " << std::dec << cycles_since_shift_.as_integral());
propose_shift(0);
}
}
@ -263,41 +273,45 @@ void IWM::run_for(const Cycles cycles) {
} break;
case ShiftMode::Writing:
if(drives_[active_drive_]->is_writing()) {
while(cycles_since_shift_ + integer_cycles >= bit_length_) {
const auto cycles_until_write = bit_length_ - cycles_since_shift_;
while(cycles_since_shift_ + integer_cycles >= bit_length_) {
const auto cycles_until_write = bit_length_ - cycles_since_shift_;
if(drives_[active_drive_]) {
drives_[active_drive_]->run_for(cycles_until_write);
// Output a flux transition if the top bit is set.
drives_[active_drive_]->write_bit(shift_register_ & 0x80);
shift_register_ <<= 1;
}
shift_register_ <<= 1;
integer_cycles -= cycles_until_write.as_integral();
cycles_since_shift_ = Cycles(0);
integer_cycles -= cycles_until_write.as_integral();
cycles_since_shift_ = Cycles(0);
--output_bits_remaining_;
if(!output_bits_remaining_) {
if(!(write_handshake_ & 0x80)) {
write_handshake_ |= 0x80;
shift_register_ = next_output_;
output_bits_remaining_ = 8;
// LOG("Next byte: " << PADHEX(2) << int(shift_register_));
} else {
write_handshake_ &= ~0x40;
drives_[active_drive_]->end_writing();
// printf("\n");
LOG("Overrun; done.");
select_shift_mode();
}
--output_bits_remaining_;
if(!output_bits_remaining_) {
if(!(write_handshake_ & 0x80)) {
shift_register_ = next_output_;
output_bits_remaining_ = 8;
// LOG("Next byte: " << PADHEX(2) << int(shift_register_));
} else {
write_handshake_ &= ~0x40;
if(drives_[active_drive_]) drives_[active_drive_]->end_writing();
LOG("Overrun; done.");
output_bits_remaining_ = 1;
}
}
cycles_since_shift_ = integer_cycles;
if(integer_cycles) {
drives_[active_drive_]->run_for(cycles_since_shift_);
// Either way, the IWM is ready for more data.
write_handshake_ |= 0x80;
}
} else {
drives_[active_drive_]->run_for(cycles);
}
// Either some bits were output, in which case cycles_since_shift_ is no 0 and
// integer_cycles is some number less than bit_length_, or none were and
// cycles_since_shift_ + integer_cycles is less than bit_length, and the new
// part should be accumulated.
cycles_since_shift_ += integer_cycles;
if(drives_[active_drive_] && integer_cycles) {
drives_[active_drive_]->run_for(cycles_since_shift_);
}
break;
@ -332,12 +346,12 @@ void IWM::select_shift_mode() {
}
// If writing mode just began, set the drive into write mode and cue up the first output byte.
if(drives_[active_drive_] && old_shift_mode != ShiftMode::Writing && shift_mode_ == ShiftMode::Writing) {
drives_[active_drive_]->begin_writing(Storage::Time(1, clock_rate_ / bit_length_.as_integral()), false);
if(old_shift_mode != ShiftMode::Writing && shift_mode_ == ShiftMode::Writing) {
if(drives_[active_drive_]) drives_[active_drive_]->begin_writing(Storage::Time(1, clock_rate_ / bit_length_.as_integral()), false);
shift_register_ = next_output_;
write_handshake_ |= 0x80 | 0x40;
output_bits_remaining_ = 8;
LOG("Seeding output with " << PADHEX(2) << shift_register_);
LOG("Seeding output with " << PADHEX(2) << int(shift_register_));
}
}
@ -351,6 +365,7 @@ void IWM::process_event(const Storage::Disk::Drive::Event &event) {
switch(event.type) {
case Storage::Disk::Track::Event::IndexHole: return;
case Storage::Disk::Track::Event::FluxTransition:
// LOG("Shifting 1 at " << std::dec << cycles_since_shift_.as_integral());
propose_shift(1);
break;
}
@ -359,12 +374,13 @@ void IWM::process_event(const Storage::Disk::Drive::Event &event) {
void IWM::propose_shift(uint8_t bit) {
// TODO: synchronous mode.
// LOG("Shifting at " << std::dec << cycles_since_shift_.as_integral());
// LOG("Shifting input");
// See above for text from the IWM patent, column 7, around line 35 onwards.
// The error_margin here implements the 'before' part of that contract.
//
// Basic effective logic: if at least 1 is fozund in the bit_length_ cycles centred
// Basic effective logic: if at least 1 is found in the bit_length_ cycles centred
// on the current expected bit delivery time as implied by cycles_since_shift_,
// shift in a 1 and start a new window wherever the first found 1 was.
//
@ -374,6 +390,7 @@ void IWM::propose_shift(uint8_t bit) {
shift_register_ = uint8_t((shift_register_ << 1) | bit);
if(shift_register_ & 0x80) {
// if(data_register_ & 0x80) LOG("Byte missed");
data_register_ = shift_register_;
shift_register_ = 0;
}
@ -386,16 +403,20 @@ void IWM::propose_shift(uint8_t bit) {
void IWM::set_drive(int slot, IWMDrive *drive) {
drives_[slot] = drive;
drive->set_event_delegate(this);
drive->set_clocking_hint_observer(this);
if(drive) {
drive->set_event_delegate(this);
drive->set_clocking_hint_observer(this);
} else {
drive_is_rotating_[slot] = false;
}
}
void IWM::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) {
const bool is_rotating = clocking != ClockingHint::Preference::None;
if(component == static_cast<ClockingHint::Source *>(drives_[0])) {
if(drives_[0] && component == static_cast<ClockingHint::Source *>(drives_[0])) {
drive_is_rotating_[0] = is_rotating;
} else {
} else if(drives_[1] && component == static_cast<ClockingHint::Source *>(drives_[1])) {
drive_is_rotating_[1] = is_rotating;
}
}

View File

@ -142,8 +142,11 @@ bool DoubleDensityDrive::read() {
return !get_is_track_zero();
case CA1|CA0: // Disk has been ejected.
// (0 = user has ejected disk)
return !has_new_disk_;
// (1 = user has ejected disk)
//
// TODO: does this really mean _user_ has ejected disk? If so then I should avoid
// changing the flag upon a programmatic eject.
return has_new_disk_;
case CA1|CA0|SEL: // Tachometer.
// (arbitrary)
@ -170,12 +173,10 @@ bool DoubleDensityDrive::read() {
case CA2|CA1|CA0|SEL: // Drive installed.
// (0 = present, 1 = missing)
//
// TODO: why do I need to return this the wrong way around for the Mac Plus?
return true;
return false;
}
}
void DoubleDensityDrive::did_set_disk() {
has_new_disk_ = true;
void DoubleDensityDrive::did_set_disk(bool did_replace) {
has_new_disk_ = did_replace;
}

View File

@ -21,7 +21,7 @@ class DoubleDensityDrive: public IWMDrive {
/*!
@returns @c true if this is an 800kb drive; @c false otherwise.
*/
bool is_800k() {
bool is_800k() const {
return is_800k_;
}
@ -39,7 +39,7 @@ class DoubleDensityDrive: public IWMDrive {
// To receive the proper notifications from Storage::Disk::Drive.
void did_step(Storage::Disk::HeadPosition to_position) final;
void did_set_disk() final;
void did_set_disk(bool) final;
const bool is_800k_;
bool has_new_disk_ = false;

View File

@ -0,0 +1,24 @@
//
// AccessType.h
// Clock Signal
//
// Created by Thomas Harte on 16/01/21.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef AccessType_h
#define AccessType_h
namespace InstructionSet {
enum class AccessType {
None,
Read,
Write,
ReadModifyWrite
};
}
#endif /* AccessType_h */

View File

@ -0,0 +1,200 @@
//
// CachingExecutor.hpp
// Clock Signal
//
// Created by Thomas Harte on 16/01/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef CachingExecutor_hpp
#define CachingExecutor_hpp
#include "Sizes.hpp"
#include <array>
#include <cstdint>
#include <limits>
#include <list>
#include <map>
#include <queue>
#include <unordered_map>
namespace InstructionSet {
/*!
A caching executor makes use of an instruction set-specific executor to cache 'performers' (i.e. function pointers)
that result from decoding.
In other words, it's almost a JIT compiler, but producing threaded code (in the Forth sense) and then incurring whatever
costs sit behind using the C ABI for calling. Since there'll always be exactly one parameter, being the specific executor,
hopefully the calling costs are acceptable.
Intended usage is for specific executors to subclass from this and declare it a friend.
TODO: determine promises re: interruption, amongst other things.
*/
template <
/// Indicates the Executor for this platform.
typename Executor,
/// Indicates the greatest value the program counter might take.
uint64_t max_address,
/// Indicates the maximum number of potential performers that will be provided.
uint64_t max_performer_count,
/// Provides the type of Instruction to expect.
typename InstructionType,
/// Indicates whether instructions should be treated as ephemeral or included in the cache.
bool retain_instructions
> class CachingExecutor {
public:
using Performer = void (Executor::*)();
using PerformerIndex = typename MinIntTypeValue<max_performer_count>::type;
using ProgramCounterType = typename MinIntTypeValue<max_address>::type;
// MARK: - Parser call-ins.
void announce_overflow(ProgramCounterType) {
/*
Should be impossible for now; this is intended to provide information
when page caching.
*/
}
void announce_instruction(ProgramCounterType, InstructionType instruction) {
// Dutifully map the instruction to a performer and keep it.
program_.push_back(static_cast<Executor *>(this)->action_for(instruction));
if constexpr (retain_instructions) {
// TODO.
}
}
protected:
// Storage for the statically-allocated list of performers. It's a bit more
// work for executors to fill this array, but subsequently performers can be
// indexed by array position, which is a lot more compact than a generic pointer.
std::array<Performer, max_performer_count+1> performers_;
ProgramCounterType program_counter_;
/*!
Moves the current point of execution to @c address, updating necessary performer caches
and doing any translation as is necessary.
*/
void set_program_counter(ProgramCounterType address) {
// Set flag to terminate any inner loop currently running through
// previously-parsed content.
has_branched_ = true;
program_counter_ = address;
// Temporary implementation: just interpret.
program_.clear();
program_index_ = 0;
static_cast<Executor *>(this)->parse(address, ProgramCounterType(max_address));
// const auto page = find_page(address);
// const auto entry = page->entry_points.find(address);
// if(entry == page->entry_points.end()) {
// // Requested segment wasn't found; check whether it was
// // within the recently translated list and otherwise
// // translate it.
// }
}
/*!
Indicates whether the processor is currently 'stopped', i.e. whether all attempts to run
should produce no activity. Some processors have such a state when waiting for
interrupts or for a reset.
*/
void set_is_stopped(bool) {}
/*!
Executes up to the next branch.
*/
void run_to_branch() {
has_branched_ = false;
for(auto index: program_) {
const auto performer = performers_[index];
(static_cast<Executor *>(this)->*performer)();
if(has_branched_) break;
}
}
/*!
Runs for @c duration; the intention is that subclasses provide a method
that is clear about units, and call this to count down in whatever units they
count down in.
*/
void run_for(int duration) {
remaining_duration_ += duration;
while(remaining_duration_ > 0) {
has_branched_ = false;
Executor *const executor = static_cast<Executor *>(this);
while(remaining_duration_ > 0 && !has_branched_) {
const auto performer = performers_[program_[program_index_]];
++program_index_;
(executor->*performer)();
}
}
}
/*!
Should be called by a specific executor to subtract from the remaining
running duration.
*/
inline void subtract_duration(int duration) {
remaining_duration_ -= duration;
}
private:
bool has_branched_ = false;
int remaining_duration_ = 0;
std::vector<PerformerIndex> program_;
size_t program_index_ = 0;
/* TODO: almost below here can be shoved off into an LRUCache object, or similar. */
// static constexpr size_t max_cached_pages = 64;
// struct Page {
// std::map<ProgramCounterType, PerformerIndex> entry_points;
// TODO: can I statically these two? Should I?
// std::vector<PerformerIndex> actions_;
// std::vector<typename std::enable_if<!std::is_same<InstructionType, void>::value, InstructionType>::type> instructions_;
// };
// std::array<Page, max_cached_pages> pages_;
// Maps from page numbers to pages.
// std::unordered_map<ProgramCounterType, Page *> cached_pages_;
// Maintains an LRU of recently-used pages in case of a need for reuse.
// std::list<ProgramCounterType> touched_pages_;
/*!
Finds or creates the page that contains @c address.
*/
/* Page *find_page(ProgramCounterType address) {
// TODO: are 1kb pages always appropriate? Is 64 the correct amount to keep?
const auto page_address = ProgramCounterType(address >> 10);
auto page = cached_pages_.find(page_address);
if(page == cached_pages_.end()) {
// Page wasn't found; either allocate a new one or
// reuse one that already exists.
if(cached_pages_.size() == max_cached_pages) {
} else {
}
} else {
// Page was found; LRU shuffle it.
}
return nullptr;
}*/
};
}
#endif /* CachingExecutor_hpp */

View File

@ -0,0 +1,89 @@
//
// Disassembler.hpp
// Clock Signal
//
// Created by Thomas Harte on 26/01/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef Disassembler_hpp
#define Disassembler_hpp
#include "Sizes.hpp"
#include <list>
#include <map>
#include <set>
namespace InstructionSet {
template <
/// Indicates the Parser for this platform.
template<typename, bool> class ParserType,
/// Indicates the greatest value the program counter might take.
uint64_t max_address,
/// Provides the type of Instruction to expect.
typename InstructionType,
/// Provides the storage size used for memory.
typename MemoryWord,
/// Provides the addressing range of memory.
typename AddressType
> class Disassembler {
public:
using ProgramCounterType = typename MinIntTypeValue<max_address>::type;
/*!
Adds the result of disassembling @c memory which is @c length @c MemoryWords long from @c start_address
to the current net total of instructions and recorded memory accesses.
*/
void disassemble(const MemoryWord *memory, ProgramCounterType location, ProgramCounterType length, ProgramCounterType start_address) {
// TODO: possibly, move some of this stuff to instruction-set specific disassemblers, analogous to
// the Executor's ownership of the Parser. That would allow handling of stateful parsing.
ParserType<decltype(*this), true> parser;
pending_entry_points_.push_back(start_address);
entry_points_.insert(start_address);
while(!pending_entry_points_.empty()) {
const auto next_entry_point = pending_entry_points_.front();
pending_entry_points_.pop_front();
if(next_entry_point >= location) {
parser.parse(*this, memory - location, next_entry_point & max_address, length + location);
}
}
}
const std::map<ProgramCounterType, InstructionType> &instructions() const {
return instructions_;
}
const std::set<ProgramCounterType> &entry_points() const {
return entry_points_;
}
void announce_overflow(ProgramCounterType) {}
void announce_instruction(ProgramCounterType address, InstructionType instruction) {
instructions_[address] = instruction;
}
void add_entry(ProgramCounterType address) {
if(entry_points_.find(address) == entry_points_.end()) {
pending_entry_points_.push_back(address);
entry_points_.insert(address);
}
}
void add_access(AddressType address, AccessType access_type) {
// TODO.
(void)address;
(void)access_type;
}
private:
std::map<ProgramCounterType, InstructionType> instructions_;
std::set<ProgramCounterType> entry_points_;
std::list<ProgramCounterType> pending_entry_points_;
};
}
#endif /* Disassembler_h */

View File

@ -0,0 +1,282 @@
//
// Decoder.cpp
// Clock Signal
//
// Created by Thomas Harte on 15/01/21.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Decoder.hpp"
#include <algorithm>
namespace InstructionSet {
namespace M50740 {
Instruction Decoder::instrucion_for_opcode(uint8_t opcode) {
switch(opcode) {
default: return Instruction(opcode);
#define Map(opcode, operation, addressing_mode) case opcode: return Instruction(Operation::operation, AddressingMode::addressing_mode, opcode);
/* 0x00 0x0f */
Map(0x00, BRK, Implied); Map(0x01, ORA, XIndirect);
Map(0x02, JSR, ZeroPageIndirect); Map(0x03, BBS0, AccumulatorRelative);
Map(0x05, ORA, ZeroPage);
Map(0x06, ASL, ZeroPage); Map(0x07, BBS0, ZeroPageRelative);
Map(0x08, PHP, Implied); Map(0x09, ORA, Immediate);
Map(0x0a, ASL, Accumulator); Map(0x0b, SEB0, Accumulator);
Map(0x0d, ORA, Absolute);
Map(0x0e, ASL, Absolute); Map(0x0f, SEB0, ZeroPage);
/* 0x10 0x1f */
Map(0x10, BPL, Relative); Map(0x11, ORA, IndirectY);
Map(0x12, CLT, Implied); Map(0x13, BBC0, AccumulatorRelative);
Map(0x15, ORA, ZeroPageX);
Map(0x16, ASL, ZeroPageX); Map(0x17, BBC0, ZeroPageRelative);
Map(0x18, CLC, Implied); Map(0x19, ORA, AbsoluteY);
Map(0x1a, DEC, Accumulator); Map(0x1b, CLB0, Accumulator);
Map(0x1d, ORA, AbsoluteX);
Map(0x1e, ASL, AbsoluteX); Map(0x1f, CLB0, ZeroPage);
/* 0x20 0x2f */
Map(0x20, JSR, Absolute); Map(0x21, AND, XIndirect);
Map(0x22, JSR, SpecialPage); Map(0x23, BBS1, AccumulatorRelative);
Map(0x24, BIT, ZeroPage); Map(0x25, AND, ZeroPage);
Map(0x26, ROL, ZeroPage); Map(0x27, BBS1, ZeroPageRelative);
Map(0x28, PLP, Implied); Map(0x29, AND, Immediate);
Map(0x2a, ROL, Accumulator); Map(0x2b, SEB1, Accumulator);
Map(0x2c, BIT, Absolute); Map(0x2d, AND, Absolute);
Map(0x2e, ROL, Absolute); Map(0x2f, SEB1, ZeroPage);
/* 0x30 0x3f */
Map(0x30, BMI, Relative); Map(0x31, AND, IndirectY);
Map(0x32, SET, Implied); Map(0x33, BBC1, AccumulatorRelative);
Map(0x35, AND, ZeroPageX);
Map(0x36, ROL, ZeroPageX); Map(0x37, BBC1, ZeroPageRelative);
Map(0x38, SEC, Implied); Map(0x39, AND, AbsoluteY);
Map(0x3a, INC, Accumulator); Map(0x3b, CLB1, Accumulator);
Map(0x3c, LDM, ImmediateZeroPage); Map(0x3d, AND, AbsoluteX);
Map(0x3e, ROL, AbsoluteX); Map(0x3f, CLB1, ZeroPage);
/* 0x40 0x4f */
Map(0x40, RTI, Implied); Map(0x41, EOR, XIndirect);
Map(0x42, STP, Implied); Map(0x43, BBS2, AccumulatorRelative);
Map(0x44, COM, ZeroPage); Map(0x45, EOR, ZeroPage);
Map(0x46, LSR, ZeroPage); Map(0x47, BBS2, ZeroPageRelative);
Map(0x48, PHA, Implied); Map(0x49, EOR, Immediate);
Map(0x4a, LSR, Accumulator); Map(0x4b, SEB2, Accumulator);
Map(0x4c, JMP, Absolute); Map(0x4d, EOR, Absolute);
Map(0x4e, LSR, Absolute); Map(0x4f, SEB2, ZeroPage);
/* 0x50 0x5f */
Map(0x50, BVC, Relative); Map(0x51, EOR, IndirectY);
Map(0x53, BBC2, AccumulatorRelative);
Map(0x55, EOR, ZeroPageX);
Map(0x56, LSR, ZeroPageX); Map(0x57, BBC2, ZeroPageRelative);
Map(0x58, CLI, Implied); Map(0x59, EOR, AbsoluteY);
Map(0x5b, CLB2, Accumulator);
Map(0x5d, EOR, AbsoluteX);
Map(0x5e, LSR, AbsoluteX); Map(0x5f, CLB2, ZeroPage);
/* 0x60 0x6f */
Map(0x60, RTS, Implied); Map(0x61, ADC, XIndirect);
Map(0x63, BBS3, AccumulatorRelative);
Map(0x64, TST, ZeroPage); Map(0x65, ADC, ZeroPage);
Map(0x66, ROR, ZeroPage); Map(0x67, BBS3, ZeroPageRelative);
Map(0x68, PLA, Implied); Map(0x69, ADC, Immediate);
Map(0x6a, ROR, Accumulator); Map(0x6b, SEB3, Accumulator);
Map(0x6c, JMP, AbsoluteIndirect); Map(0x6d, ADC, Absolute);
Map(0x6e, ROR, Absolute); Map(0x6f, SEB3, ZeroPage);
/* 0x70 0x7f */
Map(0x70, BVS, Relative); Map(0x71, ADC, IndirectY);
Map(0x73, BBC3, AccumulatorRelative);
Map(0x75, ADC, ZeroPageX);
Map(0x76, ROR, ZeroPageX); Map(0x77, BBC3, ZeroPageRelative);
Map(0x78, SEI, Implied); Map(0x79, ADC, AbsoluteY);
Map(0x7b, CLB3, Accumulator);
Map(0x7d, ADC, AbsoluteX);
Map(0x7e, ROR, AbsoluteX); Map(0x7f, CLB3, ZeroPage);
/* 0x80 0x8f */
Map(0x80, BRA, Relative); Map(0x81, STA, XIndirect);
Map(0x82, RRF, ZeroPage); Map(0x83, BBS4, AccumulatorRelative);
Map(0x84, STY, ZeroPage); Map(0x85, STA, ZeroPage);
Map(0x86, STX, ZeroPage); Map(0x87, BBS4, ZeroPageRelative);
Map(0x88, DEY, Implied);
Map(0x8a, TXA, Implied); Map(0x8b, SEB4, Accumulator);
Map(0x8c, STY, Absolute); Map(0x8d, STA, Absolute);
Map(0x8e, STX, Absolute); Map(0x8f, SEB4, ZeroPage);
/* 0x90 0x9f */
Map(0x90, BCC, Relative); Map(0x91, STA, IndirectY);
Map(0x93, BBC4, AccumulatorRelative);
Map(0x94, STY, ZeroPageX); Map(0x95, STA, ZeroPageX);
Map(0x96, STX, ZeroPageX); Map(0x97, BBC4, ZeroPageRelative);
Map(0x98, TYA, Implied); Map(0x99, STA, AbsoluteY);
Map(0x9a, TXS, Implied); Map(0x9b, CLB4, Accumulator);
Map(0x9d, ADC, AbsoluteX);
Map(0x9f, CLB4, ZeroPage);
/* 0xa0 0xaf */
Map(0xa0, LDY, Immediate); Map(0xa1, LDA, XIndirect);
Map(0xa2, LDX, Immediate); Map(0xa3, BBS5, AccumulatorRelative);
Map(0xa4, LDY, ZeroPage); Map(0xa5, LDA, ZeroPage);
Map(0xa6, LDX, ZeroPage); Map(0xa7, BBS5, ZeroPageRelative);
Map(0xa8, TAY, Implied); Map(0xa9, LDA, Immediate);
Map(0xaa, TAX, Implied); Map(0xab, SEB5, Accumulator);
Map(0xac, LDY, Absolute); Map(0xad, LDA, Absolute);
Map(0xae, LDX, Absolute); Map(0xaf, SEB5, ZeroPage);
/* 0xb0 0xbf */
Map(0xb0, BCS, Relative); Map(0xb1, STA, IndirectY);
Map(0xb2, JMP, ZeroPageIndirect); Map(0xb3, BBC5, AccumulatorRelative);
Map(0xb4, LDY, ZeroPageX); Map(0xb5, LDA, ZeroPageX);
Map(0xb6, LDX, ZeroPageY); Map(0xb7, BBC5, ZeroPageRelative);
Map(0xb8, CLV, Implied); Map(0xb9, LDA, AbsoluteY);
Map(0xba, TSX, Implied); Map(0xbb, CLB5, Accumulator);
Map(0xbc, LDY, AbsoluteX); Map(0xbd, LDA, AbsoluteX);
Map(0xbe, LDX, AbsoluteY); Map(0xbf, CLB5, ZeroPage);
/* 0xc0 0xcf */
Map(0xc0, CPY, Immediate); Map(0xc1, CMP, XIndirect);
Map(0xc2, SLW, Implied); Map(0xc3, BBS6, AccumulatorRelative);
Map(0xc4, CPY, ZeroPage); Map(0xc5, CMP, ZeroPage);
Map(0xc6, DEC, ZeroPage); Map(0xc7, BBS6, ZeroPageRelative);
Map(0xc8, INY, Implied); Map(0xc9, CMP, Immediate);
Map(0xca, DEX, Implied); Map(0xcb, SEB6, Accumulator);
Map(0xcc, CPY, Absolute); Map(0xcd, CMP, Absolute);
Map(0xce, DEC, Absolute); Map(0xcf, SEB6, ZeroPage);
/* 0xd0 0xdf */
Map(0xd0, BNE, Relative); Map(0xd1, CMP, IndirectY);
Map(0xd3, BBC6, AccumulatorRelative);
Map(0xd5, CMP, ZeroPageX);
Map(0xd6, DEC, ZeroPageX); Map(0xd7, BBC6, ZeroPageRelative);
Map(0xd8, CLD, Implied); Map(0xd9, CMP, AbsoluteY);
Map(0xdb, CLB6, Accumulator);
Map(0xdd, CMP, AbsoluteX);
Map(0xde, DEC, AbsoluteX); Map(0xdf, CLB6, ZeroPage);
/* 0xe0 0xef */
Map(0xe0, CPX, Immediate); Map(0xe1, SBC, XIndirect);
Map(0xe2, FST, Implied); Map(0xe3, BBS7, AccumulatorRelative);
Map(0xe4, CPX, ZeroPage); Map(0xe5, SBC, ZeroPage);
Map(0xe6, INC, ZeroPage); Map(0xe7, BBS7, ZeroPageRelative);
Map(0xe8, INX, Implied); Map(0xe9, SBC, Immediate);
Map(0xea, NOP, Implied); Map(0xeb, SEB7, Accumulator);
Map(0xec, CPX, Absolute); Map(0xed, SBC, Absolute);
Map(0xee, INC, Absolute); Map(0xef, SEB7, ZeroPage);
/* 0xf0 0xff */
Map(0xf0, BEQ, Relative); Map(0xf1, SBC, IndirectY);
Map(0xf3, BBC7, AccumulatorRelative);
Map(0xf5, SBC, ZeroPageX);
Map(0xf6, INC, ZeroPageX); Map(0xf7, BBC7, ZeroPageRelative);
Map(0xf8, SED, Implied); Map(0xf9, SBC, AbsoluteY);
Map(0xfb, CLB7, Accumulator);
Map(0xfd, SBC, AbsoluteX);
Map(0xfe, INC, AbsoluteX); Map(0xff, CLB7, ZeroPage);
#undef Map
}
}
std::pair<int, InstructionSet::M50740::Instruction> Decoder::decode(const uint8_t *source, size_t length) {
const uint8_t *const end = source + length;
if(phase_ == Phase::Instruction && source != end) {
const uint8_t instruction = *source;
++source;
++consumed_;
// Determine the instruction in hand, and finish now if its undefined.
instr_ = instrucion_for_opcode(instruction);
if(instr_.operation == Operation::Invalid) {
consumed_ = 0;
return std::make_pair(1, instr_);
}
// Obtain an operand size and roll onto the correct phase.
operand_size_ = size(instr_.addressing_mode);
phase_ = operand_size_ ? Phase::AwaitingOperand : Phase::ReadyToPost;
operand_bytes_ = 0;
}
if(phase_ == Phase::AwaitingOperand && source != end) {
const int outstanding_bytes = operand_size_ - operand_bytes_;
const int bytes_to_consume = std::min(int(end - source), outstanding_bytes);
consumed_ += bytes_to_consume;
source += bytes_to_consume;
operand_bytes_ += bytes_to_consume;
if(operand_size_ == operand_bytes_) {
phase_ = Phase::ReadyToPost;
} else {
return std::make_pair(-(operand_size_ - operand_bytes_), Instruction());
}
}
if(phase_ == Phase::ReadyToPost) {
const auto result = std::make_pair(consumed_, instr_);
consumed_ = 0;
phase_ = Phase::Instruction;
return result;
}
// Decoding didn't complete, without it being clear how many more bytes are required.
return std::make_pair(0, Instruction());
}
}
}

View File

@ -0,0 +1,39 @@
//
// Decoder.hpp
// Clock Signal
//
// Created by Thomas Harte on 15/01/21.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef InstructionSets_M50740_Decoder_hpp
#define InstructionSets_M50740_Decoder_hpp
#include "Instruction.hpp"
#include <cstddef>
#include <utility>
namespace InstructionSet {
namespace M50740 {
class Decoder {
public:
std::pair<int, Instruction> decode(const uint8_t *source, size_t length);
Instruction instrucion_for_opcode(uint8_t opcode);
private:
enum class Phase {
Instruction,
AwaitingOperand,
ReadyToPost
} phase_ = Phase::Instruction;
int operand_size_ = 0, operand_bytes_ = 0;
int consumed_ = 0;
Instruction instr_;
};
}
}
#endif /* InstructionSets_M50740_Decoder_hpp */

View File

@ -0,0 +1,838 @@
//
// Executor.hpp
// Clock Signal
//
// Created by Thomas Harte on 16/1/21.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Executor.hpp"
#include <algorithm>
#include <cassert>
#include <cstring>
#include "../../Machines/Utility/MemoryFuzzer.hpp"
#define LOG_PREFIX "[M50740] "
#include "../../Outputs/Log.hpp"
using namespace InstructionSet::M50740;
namespace {
constexpr int port_remap[] = {0, 1, 2, 0, 3};
}
Executor::Executor(PortHandler &port_handler) : port_handler_(port_handler) {
// Cut down the list of all generated performers to those the processor actually uses, and install that
// for future referencing by action_for.
Decoder decoder;
for(size_t c = 0; c < 256; c++) {
const auto instruction = decoder.instrucion_for_opcode(uint8_t(c));
// Treat invalid as NOP, because I've got to do _something_.
if(instruction.operation == Operation::Invalid) {
performers_[c] = performer_lookup_.performer(Operation::NOP, instruction.addressing_mode);
} else {
performers_[c] = performer_lookup_.performer(instruction.operation, instruction.addressing_mode);
}
}
// Fuzz RAM; then set anything that may be replaced by ROM to FF.
Memory::Fuzz(memory_);
memset(&memory_[0x100], 0xff, memory_.size() - 0x100);
}
void Executor::set_rom(const std::vector<uint8_t> &rom) {
// Copy into place, and reset.
const auto length = std::min(size_t(0x1000), rom.size());
memcpy(&memory_[0x2000 - length], rom.data(), length);
reset();
}
void Executor::run_for(Cycles cycles) {
// The incoming clock is divided by four; the local cycles_ count
// ensures that fractional parts are kept track of.
cycles_ += cycles;
CachingExecutor::run_for(cycles_.divide(Cycles(4)).as<int>());
}
void Executor::reset() {
// Just jump to the reset vector.
set_program_counter(uint16_t(memory_[0x1ffe] | (memory_[0x1fff] << 8)));
}
void Executor::set_interrupt_line(bool line) {
// Super hack: interrupt now, if permitted. Otherwise do nothing.
// So this will fail to catch enabling of interrupts while the line
// is active, amongst other things.
if(interrupt_line_ != line) {
interrupt_line_ = line;
// TODO: verify interrupt connection. Externally, but stubbed off here.
// if(!interrupt_disable_ && line) {
// perform_interrupt<false>(0x1ff4);
// }
}
}
uint8_t Executor::read(uint16_t address) {
address &= 0x1fff;
// Deal with RAM and ROM accesses quickly.
if(address < 0x60 || address >= 0x100) return memory_[address];
port_handler_.run_ports_for(cycles_since_port_handler_.flush<Cycles>());
switch(address) {
default:
LOG("Unrecognised read from " << PADHEX(4) << address);
return 0xff;
// "Port R"; sixteen four-bit ports
case 0xd0: case 0xd1: case 0xd2: case 0xd3: case 0xd4: case 0xd5: case 0xd6: case 0xd7:
case 0xd8: case 0xd9: case 0xda: case 0xdb: case 0xdc: case 0xdd: case 0xde: case 0xdf:
LOG("Unimplemented Port R read from " << PADHEX(4) << address);
return 0x00;
// Ports P0P3.
case 0xe0: case 0xe2:
case 0xe4: case 0xe8: {
const int port = port_remap[(address - 0xe0) >> 1];
const uint8_t input = port_handler_.get_port_input(port);
// In the direction registers, a 0 indicates input, a 1 indicates output.
return (input &~ port_directions_[port]) | (port_outputs_[port] & port_directions_[port]);
}
case 0xe1: case 0xe3:
case 0xe5: case 0xe9:
return port_directions_[port_remap[(address - 0xe0) >> 1]];
// Timers.
case 0xf9: return prescalers_[0].value;
case 0xfa: return timers_[0].value;
case 0xfb: return timers_[1].value;
case 0xfc: return prescalers_[1].value;
case 0xfd: return timers_[2].value;
case 0xfe: return interrupt_control_;
case 0xff: return timer_control_;
}
}
void Executor::set_port_output(int port) {
// Force 'output' to a 1 anywhere that a bit is set as input.
port_handler_.set_port_output(port, port_outputs_[port] | ~port_directions_[port]);
}
void Executor::write(uint16_t address, uint8_t value) {
address &= 0x1fff;
// RAM writes are easy.
if(address < 0x60) {
memory_[address] = value;
return;
}
// ROM 'writes' are almost as easy (albeit unexpected).
if(address >= 0x100) {
LOG("Attempted ROM write of " << PADHEX(2) << value << " to " << PADHEX(4) << address);
return;
}
// Push time to the port handler.
port_handler_.run_ports_for(cycles_since_port_handler_.flush<Cycles>());
switch(address) {
default:
LOG("Unrecognised write of " << PADHEX(2) << value << " to " << PADHEX(4) << address);
break;
// "Port R"; sixteen four-bit ports
case 0xd0: case 0xd1: case 0xd2: case 0xd3: case 0xd4: case 0xd5: case 0xd6: case 0xd7:
case 0xd8: case 0xd9: case 0xda: case 0xdb: case 0xdc: case 0xdd: case 0xde: case 0xdf:
LOG("Unimplemented Port R write of " << PADHEX(2) << value << " from " << PADHEX(4) << address);
break;
// Ports P0P3.
case 0xe0: case 0xe2:
case 0xe4: case 0xe8: {
const int port = port_remap[(address - 0xe0) >> 1];
port_outputs_[port] = value;
set_port_output(port);
}
break;
case 0xe1: case 0xe3:
case 0xe5: case 0xe9: {
const int port = port_remap[(address - 0xe0) >> 1];
port_directions_[port] = value;
set_port_output(port);
} break;
// Timers.
//
// Reloading of value with the reload value is a guess, based upon what I take
// to be the intended usage of timer 2 in handling key repeat on the Apple IIgs.
case 0xf9: prescalers_[0].value = prescalers_[0].reload_value = value; break;
case 0xfa: timers_[0].value = timers_[0].reload_value = value; break;
case 0xfb: timers_[1].value = timers_[1].reload_value = value; break;
case 0xfc: prescalers_[1].value = prescalers_[1].reload_value = value; break;
case 0xfd: timers_[2].value = timers_[2].reload_value = value; break;
case 0xfe: interrupt_control_ = value; break;
case 0xff: timer_control_ = value; break;
}
}
void Executor::push(uint8_t value) {
write(s_, value);
--s_;
}
uint8_t Executor::pull() {
++s_;
return read(s_);
}
void Executor::set_flags(uint8_t flags) {
negative_result_ = flags;
overflow_result_ = uint8_t(flags << 1);
index_mode_ = flags & 0x20;
decimal_mode_ = flags & 0x08;
interrupt_disable_ = flags & 0x04;
zero_result_ = !(flags & 0x02);
carry_flag_ = flags & 0x01;
}
uint8_t Executor::flags() {
return
(negative_result_ & 0x80) |
((overflow_result_ & 0x80) >> 1) |
(index_mode_ ? 0x20 : 0x00) |
(decimal_mode_ ? 0x08 : 0x00) |
interrupt_disable_ |
(zero_result_ ? 0x00 : 0x02) |
carry_flag_;
}
template<bool is_brk> inline void Executor::perform_interrupt(uint16_t vector) {
// BRK has an unused operand.
++program_counter_;
push(uint8_t(program_counter_ >> 8));
push(uint8_t(program_counter_ & 0xff));
push(flags() | (is_brk ? 0x10 : 0x00));
set_program_counter(uint16_t(memory_[vector] | (memory_[vector+1] << 8)));
}
template <Operation operation, AddressingMode addressing_mode> void Executor::perform() {
// Post cycle cost; this emulation _does not provide accurate timing_.
#define TLength(mode, base) case AddressingMode::mode: subtract_duration(base + t_lengths[index_mode_]); break;
#define Length(mode, base) case AddressingMode::mode: subtract_duration(base); break;
switch(operation) {
case Operation::ADC: case Operation::AND: case Operation::CMP: case Operation::EOR:
case Operation::LDA: case Operation::ORA: case Operation::SBC:
{
constexpr int t_lengths[] = {
0,
operation == Operation::LDA ? 2 : (operation == Operation::CMP ? 1 : 3)
};
switch(addressing_mode) {
TLength(XIndirect, 6);
TLength(ZeroPage, 3);
TLength(Immediate, 2);
TLength(Absolute, 4);
TLength(IndirectY, 6);
TLength(ZeroPageX, 4);
TLength(AbsoluteY, 5);
TLength(AbsoluteX, 5);
default: assert(false);
}
} break;
case Operation::ASL: case Operation::DEC: case Operation::INC: case Operation::LSR:
case Operation::ROL: case Operation::ROR:
switch(addressing_mode) {
Length(ZeroPage, 5);
Length(Accumulator, 2);
Length(Absolute, 6);
Length(ZeroPageX, 6);
Length(AbsoluteX, 7);
default: assert(false);
}
break;
case Operation::BBC0: case Operation::BBC1: case Operation::BBC2: case Operation::BBC3:
case Operation::BBC4: case Operation::BBC5: case Operation::BBC6: case Operation::BBC7:
case Operation::BBS0: case Operation::BBS1: case Operation::BBS2: case Operation::BBS3:
case Operation::BBS4: case Operation::BBS5: case Operation::BBS6: case Operation::BBS7:
switch(addressing_mode) {
Length(AccumulatorRelative, 4);
Length(ZeroPageRelative, 5);
default: assert(false);
}
break;
case Operation::BPL: case Operation::BMI: case Operation::BEQ: case Operation::BNE:
case Operation::BCS: case Operation::BCC: case Operation::BVS: case Operation::BVC:
case Operation::INX: case Operation::INY:
subtract_duration(2);
break;
case Operation::CPX: case Operation::CPY: case Operation::BIT: case Operation::LDX:
case Operation::LDY:
switch(addressing_mode) {
Length(Immediate, 2);
Length(ZeroPage, 3);
Length(Absolute, 4);
Length(ZeroPageX, 4);
Length(ZeroPageY, 4);
Length(AbsoluteX, 5);
Length(AbsoluteY, 5);
default: assert(false);
}
break;
case Operation::BRA: subtract_duration(4); break;
case Operation::BRK: subtract_duration(7); break;
case Operation::CLB0: case Operation::CLB1: case Operation::CLB2: case Operation::CLB3:
case Operation::CLB4: case Operation::CLB5: case Operation::CLB6: case Operation::CLB7:
case Operation::SEB0: case Operation::SEB1: case Operation::SEB2: case Operation::SEB3:
case Operation::SEB4: case Operation::SEB5: case Operation::SEB6: case Operation::SEB7:
switch(addressing_mode) {
Length(Accumulator, 2);
Length(ZeroPage, 5);
default: assert(false);
}
break;
case Operation::CLC: case Operation::CLD: case Operation::CLT: case Operation::CLV:
case Operation::CLI:
case Operation::DEX: case Operation::DEY: case Operation::FST: case Operation::NOP:
case Operation::SEC: case Operation::SED: case Operation::SEI: case Operation::SET:
case Operation::SLW: case Operation::STP: case Operation::TAX: case Operation::TAY:
case Operation::TSX: case Operation::TXA: case Operation::TXS: case Operation::TYA:
subtract_duration(2);
break;
case Operation::COM: subtract_duration(5); break;
case Operation::JMP:
switch(addressing_mode) {
Length(Absolute, 3);
Length(AbsoluteIndirect, 5);
Length(ZeroPageIndirect, 4);
default: assert(false);
}
break;
case Operation::JSR:
switch(addressing_mode) {
Length(ZeroPageIndirect, 7);
Length(Absolute, 6);
Length(SpecialPage, 5);
default: assert(false);
}
break;
case Operation::LDM: subtract_duration(4); break;
case Operation::PHA: case Operation::PHP: case Operation::TST:
subtract_duration(3);
break;
case Operation::PLA: case Operation::PLP:
subtract_duration(4);
break;
case Operation::RRF: subtract_duration(8); break;
case Operation::RTI: subtract_duration(6); break;
case Operation::RTS: subtract_duration(6); break;
case Operation::STA: case Operation::STX: case Operation::STY:
switch(addressing_mode) {
Length(XIndirect, 7);
Length(ZeroPage, 4);
Length(Absolute, 5);
Length(IndirectY, 7);
Length(ZeroPageX, 5);
Length(ZeroPageY, 5);
Length(AbsoluteY, 6);
Length(AbsoluteX, 6);
default: assert(false);
}
break;
default: assert(false);
}
// Deal with all modes that don't access memory up here;
// those that access memory will go through a slightly longer
// sequence below that wraps the address and checks whether
// a write is valid [if required].
unsigned int address;
#define next8() memory_[(program_counter_ + 1) & 0x1fff]
#define next16() uint16_t(memory_[(program_counter_ + 1) & 0x1fff] | (memory_[(program_counter_ + 2) & 0x1fff] << 8))
// Underlying assumption below: the instruction stream will never
// overlap with IO ports.
switch(addressing_mode) {
// Addressing modes with no further memory access.
case AddressingMode::Implied:
perform<operation>(nullptr);
++program_counter_;
return;
case AddressingMode::Accumulator:
perform<operation>(&a_);
++program_counter_;
return;
case AddressingMode::Immediate:
perform<operation>(&next8());
program_counter_ += 2;
return;
// Special-purpose addressing modes.
case AddressingMode::Relative:
address = unsigned(program_counter_ + 1 + size(addressing_mode) + int8_t(next8()));
break;
case AddressingMode::SpecialPage: address = 0x1f00 | next8(); break;
case AddressingMode::ImmediateZeroPage:
// LDM only...
write(memory_[(program_counter_+2)&0x1fff], memory_[(program_counter_+1)&0x1fff]);
program_counter_ += 1 + size(addressing_mode);
return;
case AddressingMode::AccumulatorRelative:
case AddressingMode::ZeroPageRelative: {
// Order of bytes is: (i) zero page address; (ii) relative jump.
uint8_t value;
if constexpr (addressing_mode == AddressingMode::AccumulatorRelative) {
value = a_;
address = unsigned(program_counter_ + 1 + size(addressing_mode) + int8_t(next8()));
} else {
value = read(next8());
address = unsigned(program_counter_ + 1 + size(addressing_mode) + int8_t(memory_[(program_counter_+2)&0x1fff]));
}
program_counter_ += 1 + size(addressing_mode);
switch(operation) {
case Operation::BBS0: case Operation::BBS1: case Operation::BBS2: case Operation::BBS3:
case Operation::BBS4: case Operation::BBS5: case Operation::BBS6: case Operation::BBS7: {
if constexpr (operation >= Operation::BBS0 && operation <= Operation::BBS7) {
constexpr uint8_t mask = 1 << (int(operation) - int(Operation::BBS0));
if(value & mask) {
set_program_counter(uint16_t(address));
subtract_duration(2);
}
}
} return;
case Operation::BBC0: case Operation::BBC1: case Operation::BBC2: case Operation::BBC3:
case Operation::BBC4: case Operation::BBC5: case Operation::BBC6: case Operation::BBC7: {
if constexpr (operation >= Operation::BBC0 && operation <= Operation::BBC7) {
constexpr uint8_t mask = 1 << (int(operation) - int(Operation::BBC0));
if(!(value & mask)) {
set_program_counter(uint16_t(address));
subtract_duration(2);
}
}
} return;
default: assert(false);
}
} break;
// Addressing modes with a memory access.
case AddressingMode::Absolute: address = next16(); break;
case AddressingMode::AbsoluteX: address = next16() + x_; break;
case AddressingMode::AbsoluteY: address = next16() + y_; break;
case AddressingMode::ZeroPage: address = next8(); break;
case AddressingMode::ZeroPageX: address = (next8() + x_) & 0xff; break;
case AddressingMode::ZeroPageY: address = (next8() + y_) & 0xff; break;
case AddressingMode::ZeroPageIndirect:
address = next8();
address = unsigned(memory_[address] | (memory_[(address + 1) & 0xff] << 8));
break;
case AddressingMode::XIndirect:
address = (next8() + x_) & 0xff;
address = unsigned(memory_[address] | (memory_[(address + 1)&0xff] << 8));
break;
case AddressingMode::IndirectY:
address = unsigned((memory_[next8()] | (memory_[(next8()+1)&0xff] << 8)) + y_);
break;
case AddressingMode::AbsoluteIndirect:
address = next16();
address = unsigned(memory_[address & 0x1fff] | (memory_[(address + 1) & 0x1fff] << 8));
break;
default:
assert(false);
}
#undef next16
#undef next8
program_counter_ += 1 + size(addressing_mode);
// Check for a branch; those don't go through the memory accesses below.
switch(operation) {
case Operation::BRA: case Operation::JMP:
set_program_counter(uint16_t(address));
return;
case Operation::JSR: {
// Push one less than the actual return address.
const auto return_address = program_counter_ - 1;
push(uint8_t(return_address >> 8));
push(uint8_t(return_address & 0xff));
set_program_counter(uint16_t(address));
} return;
#define Bcc(c) if(c) { set_program_counter(uint16_t(address)); subtract_duration(2); } return
case Operation::BPL: Bcc(!(negative_result_&0x80));
case Operation::BMI: Bcc(negative_result_&0x80);
case Operation::BEQ: Bcc(!zero_result_);
case Operation::BNE: Bcc(zero_result_);
case Operation::BCS: Bcc(carry_flag_);
case Operation::BCC: Bcc(!carry_flag_);
case Operation::BVS: Bcc(overflow_result_ & 0x80);
case Operation::BVC: Bcc(!(overflow_result_ & 0x80));
#undef Bcc
default: break;
}
assert(access_type(operation) != AccessType::None);
if constexpr(access_type(operation) == AccessType::Read) {
uint8_t source = read(uint16_t(address));
perform<operation>(&source);
return;
}
uint8_t value;
if constexpr(access_type(operation) == AccessType::ReadModifyWrite) {
value = read(uint16_t(address));
} else {
value = 0xff;
}
perform<operation>(&value);
write(uint16_t(address), value);
}
template <Operation operation> void Executor::perform(uint8_t *operand [[maybe_unused]]) {
#define set_nz(a) negative_result_ = zero_result_ = (a)
switch(operation) {
case Operation::LDA:
if(index_mode_) {
write(x_, *operand);
set_nz(*operand);
} else {
set_nz(a_ = *operand);
}
break;
case Operation::LDX: set_nz(x_ = *operand); break;
case Operation::LDY: set_nz(y_ = *operand); break;
case Operation::STA: *operand = a_; break;
case Operation::STX: *operand = x_; break;
case Operation::STY: *operand = y_; break;
case Operation::TXA: set_nz(a_ = x_); break;
case Operation::TYA: set_nz(a_ = y_); break;
case Operation::TXS: s_ = x_; break;
case Operation::TAX: set_nz(x_ = a_); break;
case Operation::TAY: set_nz(y_ = a_); break;
case Operation::TSX: set_nz(x_ = s_); break;
case Operation::SEB0: case Operation::SEB1: case Operation::SEB2: case Operation::SEB3:
case Operation::SEB4: case Operation::SEB5: case Operation::SEB6: case Operation::SEB7:
if constexpr(operation >= Operation::SEB0 && operation <= Operation::SEB7) {
*operand |= 1 << (int(operation) - int(Operation::SEB0));
}
break;
case Operation::CLB0: case Operation::CLB1: case Operation::CLB2: case Operation::CLB3:
case Operation::CLB4: case Operation::CLB5: case Operation::CLB6: case Operation::CLB7:
if constexpr(operation >= Operation::CLB0 && operation <= Operation::CLB7) {
*operand &= ~(1 << (int(operation) - int(Operation::CLB0)));
}
break;
case Operation::CLI: interrupt_disable_ = 0x00; break;
case Operation::SEI: interrupt_disable_ = 0x04; break;
case Operation::CLT: index_mode_ = false; break;
case Operation::SET: index_mode_ = true; break;
case Operation::CLD: decimal_mode_ = false; break;
case Operation::SED: decimal_mode_ = true; break;
case Operation::CLC: carry_flag_ = 0; break;
case Operation::SEC: carry_flag_ = 1; break;
case Operation::CLV: overflow_result_ = 0; break;
case Operation::DEX: --x_; set_nz(x_); break;
case Operation::INX: ++x_; set_nz(x_); break;
case Operation::DEY: --y_; set_nz(y_); break;
case Operation::INY: ++y_; set_nz(y_); break;
case Operation::DEC: --*operand; set_nz(*operand); break;
case Operation::INC: ++*operand; set_nz(*operand); break;
case Operation::RTS: {
uint16_t target = pull();
target |= pull() << 8;
set_program_counter(target+1);
--program_counter_; // To undo the unavoidable increment
// after exiting from here.
} break;
case Operation::RTI: {
set_flags(pull());
uint16_t target = pull();
target |= pull() << 8;
set_program_counter(target);
--program_counter_; // To undo the unavoidable increment
// after exiting from here.
} break;
case Operation::BRK:
perform_interrupt<true>(0x1ff4);
--program_counter_; // To undo the unavoidable increment
// after exiting from here.
break;
case Operation::STP: set_is_stopped(true); break;
case Operation::COM: set_nz(*operand ^= 0xff); break;
case Operation::FST: case Operation::SLW: case Operation::NOP:
// TODO: communicate FST and SLW onwards, I imagine. Find out what they interface with.
break;
case Operation::PHA: push(a_); break;
case Operation::PHP: push(flags()); break;
case Operation::PLA: set_nz(a_ = pull()); break;
case Operation::PLP: set_flags(pull()); break;
case Operation::ASL:
carry_flag_ = *operand >> 7;
*operand <<= 1;
set_nz(*operand);
break;
case Operation::LSR:
carry_flag_ = *operand & 1;
*operand >>= 1;
set_nz(*operand);
break;
case Operation::ROL: {
const uint8_t temp8 = uint8_t((*operand << 1) | carry_flag_);
carry_flag_ = *operand >> 7;
set_nz(*operand = temp8);
} break;
case Operation::ROR: {
const uint8_t temp8 = uint8_t((*operand >> 1) | (carry_flag_ << 7));
carry_flag_ = *operand & 1;
set_nz(*operand = temp8);
} break;
case Operation::RRF:
*operand = uint8_t((*operand >> 4) | (*operand << 4));
break;
case Operation::BIT:
zero_result_ = *operand & a_;
negative_result_ = *operand;
overflow_result_ = uint8_t(*operand << 1);
break;
case Operation::TST:
set_nz(*operand);
break;
/*
Operations affected by the index mode flag: ADC, AND, CMP, EOR, LDA, ORA, and SBC.
*/
#define index(op) \
if(index_mode_) { \
uint8_t t = read(x_); \
op(t); \
write(x_, t); \
} else { \
op(a_); \
}
#define op_ora(x) set_nz(x |= *operand)
#define op_and(x) set_nz(x &= *operand)
#define op_eor(x) set_nz(x ^= *operand)
case Operation::ORA: index(op_ora); break;
case Operation::AND: index(op_and); break;
case Operation::EOR: index(op_eor); break;
#undef op_eor
#undef op_and
#undef op_ora
#undef index
#define op_cmp(x) { \
const uint16_t temp16 = x - *operand; \
set_nz(uint8_t(temp16)); \
carry_flag_ = (~temp16 >> 8)&1; \
}
case Operation::CMP:
if(index_mode_) {
op_cmp(read(x_));
} else {
op_cmp(a_);
}
break;
case Operation::CPX: op_cmp(x_); break;
case Operation::CPY: op_cmp(y_); break;
#undef op_cmp
case Operation::SBC:
case Operation::ADC: {
const uint8_t a = index_mode_ ? read(x_) : a_;
if(decimal_mode_) {
if(operation == Operation::ADC) {
uint16_t partials = 0;
int result = carry_flag_;
#define nibble(mask, limit, adjustment, carry) \
result += (a & mask) + (*operand & mask); \
partials += result & mask; \
if(result >= limit) result = ((result + (adjustment)) & (carry - 1)) + carry;
nibble(0x000f, 0x000a, 0x0006, 0x00010);
nibble(0x00f0, 0x00a0, 0x0060, 0x00100);
#undef nibble
overflow_result_ = uint8_t((partials ^ a) & (partials ^ *operand));
set_nz(uint8_t(result));
carry_flag_ = (result >> 8) & 1;
} else {
unsigned int result = 0;
unsigned int borrow = carry_flag_ ^ 1;
const uint16_t decimal_result = uint16_t(a - *operand - borrow);
#define nibble(mask, adjustment, carry) \
result += (a & mask) - (*operand & mask) - borrow; \
if(result > mask) result -= adjustment; \
borrow = (result > mask) ? carry : 0; \
result &= (carry - 1);
nibble(0x000f, 0x0006, 0x00010);
nibble(0x00f0, 0x0060, 0x00100);
#undef nibble
overflow_result_ = uint8_t((decimal_result ^ a) & (~decimal_result ^ *operand));
set_nz(uint8_t(result));
carry_flag_ = ((borrow >> 8)&1)^1;
}
} else {
int result;
if(operation == Operation::ADC) {
result = int(a + *operand + carry_flag_);
overflow_result_ = uint8_t((result ^ a) & (result ^ *operand));
} else {
result = int(a + ~*operand + carry_flag_);
overflow_result_ = uint8_t((result ^ a) & (result ^ ~*operand));
}
set_nz(uint8_t(result));
carry_flag_ = (result >> 8) & 1;
}
if(index_mode_) {
write(x_, a);
} else {
a_ = a;
}
} break;
/*
Already removed from the instruction stream:
* all branches and jumps;
* LDM.
*/
default:
LOG("Unimplemented operation: " << operation);
assert(false);
}
#undef set_nz
}
inline void Executor::subtract_duration(int duration) {
// Pass along.
CachingExecutor::subtract_duration(duration);
// Update count for potential port accesses.
cycles_since_port_handler_ += Cycles(duration);
// Update timer 1 and 2 prescaler.
constexpr int t12_divider = 4; // A divide by 4 has already been applied before counting instruction lengths; therefore
// this additional divide by 4 produces the correct net divide by 16.
timer_divider_ += duration;
const int t12_ticks = update_timer(prescalers_[0], timer_divider_ / t12_divider);
timer_divider_ &= (t12_divider-1);
// Update timers 1 and 2. TODO: interrupts (elsewhere?).
if(update_timer(timers_[0], t12_ticks)) interrupt_control_ |= 0x20;
if(update_timer(timers_[1], t12_ticks)) interrupt_control_ |= 0x08;
// If timer X is disabled, stop.
if(timer_control_&0x20) {
return;
}
// Update timer X prescaler.
switch(timer_control_ & 0x0c) {
default: {
const int tx_ticks = update_timer(prescalers_[1], t12_ticks); // TODO: don't hard code this. And is this even right?
if(update_timer(timers_[2], tx_ticks))
timer_control_ |= 0x80; // TODO: interrupt result of this.
} break;
case 0x04:
LOG("TODO: Timer X; Pulse output mode");
break;
case 0x08:
LOG("TODO: Timer X; Event counter mode");
break;
case 0x0c:
LOG("TODO: Timer X; Pulse width measurement mode");
break;
}
}
inline int Executor::update_timer(Timer &timer, int count) {
const int next_value = timer.value - count;
if(next_value < 0) {
// Determine how many reloads were required to get above zero.
const int reload_value = timer.reload_value ? timer.reload_value : 256;
const int underflow_count = 1 - next_value / reload_value;
timer.value = uint8_t((next_value % reload_value) + timer.reload_value);
return underflow_count;
}
timer.value = uint8_t(next_value);
return 0;
}
uint8_t Executor::get_output_mask(int port) {
return port_directions_[port];
}

View File

@ -0,0 +1,180 @@
//
// Executor.h
// Clock Signal
//
// Created by Thomas Harte on 16/01/21.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef Executor_h
#define Executor_h
#include "Instruction.hpp"
#include "Parser.hpp"
#include "../CachingExecutor.hpp"
#include "../../ClockReceiver/ClockReceiver.hpp"
#include <array>
#include <cstdint>
#include <vector>
namespace InstructionSet {
namespace M50740 {
class Executor;
using CachingExecutor = CachingExecutor<Executor, 0x1fff, 255, Instruction, false>;
struct PortHandler {
virtual void run_ports_for(Cycles) = 0;
virtual void set_port_output(int port, uint8_t value) = 0;
virtual uint8_t get_port_input(int port) = 0;
};
/*!
Executes M50740 code subject to heavy limitations:
* the instruction stream cannot run across any of the specialised IO addresses; and
* timing is correct to whole-opcode boundaries only.
*/
class Executor: public CachingExecutor {
public:
Executor(PortHandler &);
void set_rom(const std::vector<uint8_t> &rom);
void reset();
void set_interrupt_line(bool);
uint8_t get_output_mask(int port);
/*!
Runs, in discrete steps, the minimum number of instructions as it takes to complete at least @c cycles.
*/
void run_for(Cycles cycles);
private:
// MARK: - CachingExecutor-facing interface.
friend CachingExecutor;
/*!
Maps instructions to performers; called by the CachingExecutor and for this instruction set, extremely trivial.
*/
inline PerformerIndex action_for(Instruction instruction) {
// This is a super-simple processor, so the opcode can be used directly to index the performers.
return instruction.opcode;
}
/*!
Parses from @c start and no later than @c max_address, using the CachingExecutor as a target.
*/
inline void parse(uint16_t start, uint16_t closing_bound) {
Parser<Executor, false> parser;
parser.parse(*this, &memory_[0], start & 0x1fff, closing_bound);
}
private:
// MARK: - Internal framework for generator performers.
/*!
Provides dynamic lookup of @c perform(Executor*).
*/
class PerformerLookup {
public:
PerformerLookup() {
fill<int(MinOperation)>(performers_);
}
Performer performer(Operation operation, AddressingMode addressing_mode) {
const auto index =
(int(operation) - MinOperation) * (1 + MaxAddressingMode - MinAddressingMode) +
(int(addressing_mode) - MinAddressingMode);
return performers_[index];
}
private:
Performer performers_[(1 + MaxAddressingMode - MinAddressingMode) * (1 + MaxOperation - MinOperation)];
template<int operation, int addressing_mode> void fill_operation(Performer *target) {
*target = &Executor::perform<Operation(operation), AddressingMode(addressing_mode)>;
if constexpr (addressing_mode+1 <= MaxAddressingMode) {
fill_operation<operation, addressing_mode+1>(target + 1);
}
}
template<int operation> void fill(Performer *target) {
fill_operation<operation, int(MinAddressingMode)>(target);
target += 1 + MaxAddressingMode - MinAddressingMode;
if constexpr (operation+1 <= MaxOperation) {
fill<operation+1>(target);
}
}
};
inline static PerformerLookup performer_lookup_;
/*!
Performs @c operation using @c operand as the value fetched from memory, if any.
*/
template <Operation operation> void perform(uint8_t *operand);
/*!
Performs @c operation in @c addressing_mode.
*/
template <Operation operation, AddressingMode addressing_mode> void perform();
private:
// MARK: - Instruction set state.
// Memory.
std::array<uint8_t, 0x2000> memory_;
// Registers.
uint8_t a_ = 0, x_ = 0, y_ = 0, s_ = 0;
uint8_t negative_result_ = 0;
uint8_t zero_result_ = 0;
uint8_t interrupt_disable_ = 0x04;
uint8_t carry_flag_ = 0;
uint8_t overflow_result_ = 0;
bool index_mode_ = false;
bool decimal_mode_ = false;
// IO ports.
uint8_t port_directions_[4] = {0x00, 0x00, 0x00, 0x00};
uint8_t port_outputs_[4] = {0xff, 0xff, 0xff, 0xff};
// Timers.
struct Timer {
uint8_t value = 0xff, reload_value = 0xff;
};
int timer_divider_ = 0;
Timer timers_[3], prescalers_[2];
inline int update_timer(Timer &timer, int count);
// Interrupt and timer control.
uint8_t interrupt_control_ = 0, timer_control_ = 0;
bool interrupt_line_ = false;
// Access helpers.
inline uint8_t read(uint16_t address);
inline void write(uint16_t address, uint8_t value);
inline void push(uint8_t value);
inline uint8_t pull();
inline void set_flags(uint8_t);
inline uint8_t flags();
template<bool is_brk> inline void perform_interrupt(uint16_t vector);
inline void set_port_output(int port);
// MARK: - Execution time
Cycles cycles_;
Cycles cycles_since_port_handler_;
PortHandler &port_handler_;
inline void subtract_duration(int duration);
};
}
}
#endif /* Executor_h */

View File

@ -0,0 +1,242 @@
//
// Instruction.hpp
// Clock Signal
//
// Created by Thomas Harte on 15/01/21.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef InstructionSets_M50740_Instruction_h
#define InstructionSets_M50740_Instruction_h
#include <cstdint>
#include <iomanip>
#include <string>
#include <sstream>
#include "../AccessType.hpp"
namespace InstructionSet {
namespace M50740 {
enum class AddressingMode {
Implied, Accumulator, Immediate,
Absolute, AbsoluteX, AbsoluteY,
ZeroPage, ZeroPageX, ZeroPageY,
XIndirect, IndirectY,
Relative,
AbsoluteIndirect, ZeroPageIndirect,
SpecialPage,
ImmediateZeroPage,
AccumulatorRelative, ZeroPageRelative
};
static constexpr auto MaxAddressingMode = int(AddressingMode::ZeroPageRelative);
static constexpr auto MinAddressingMode = int(AddressingMode::Implied);
constexpr int size(AddressingMode mode) {
// This is coupled to the AddressingMode list above; be careful!
constexpr int sizes[] = {
0, 0, 1,
2, 2, 2,
1, 1, 1,
1, 1,
1,
2, 1,
1,
2,
1, 2
};
static_assert(sizeof(sizes)/sizeof(*sizes) == int(MaxAddressingMode) + 1);
return sizes[int(mode)];
}
enum class Operation: uint8_t {
Invalid,
// Operations that don't access memory.
BBC0, BBC1, BBC2, BBC3, BBC4, BBC5, BBC6, BBC7,
BBS0, BBS1, BBS2, BBS3, BBS4, BBS5, BBS6, BBS7,
BCC, BCS,
BEQ, BMI, BNE, BPL,
BVC, BVS, BRA, BRK,
JMP, JSR,
RTI, RTS,
CLC, CLD, CLI, CLT, CLV,
SEC, SED, SEI, SET,
INX, INY, DEX, DEY,
FST, SLW,
NOP,
PHA, PHP, PLA, PLP,
STP,
TAX, TAY, TSX, TXA,
TXS, TYA,
// Read operations.
ADC, SBC,
AND, ORA, EOR, BIT,
CMP, CPX, CPY,
LDA, LDX, LDY,
TST,
// Read-modify-write operations.
ASL, LSR,
CLB0, CLB1, CLB2, CLB3, CLB4, CLB5, CLB6, CLB7,
SEB0, SEB1, SEB2, SEB3, SEB4, SEB5, SEB6, SEB7,
COM,
DEC, INC,
ROL, ROR, RRF,
// Write operations.
LDM,
STA, STX, STY,
};
static constexpr auto MaxOperation = int(Operation::STY);
static constexpr auto MinOperation = int(Operation::BBC0);
constexpr AccessType access_type(Operation operation) {
if(operation < Operation::ADC) return AccessType::None;
if(operation < Operation::ASL) return AccessType::Read;
if(operation < Operation::LDM) return AccessType::ReadModifyWrite;
return AccessType::Write;
}
constexpr bool uses_index_mode(Operation operation) {
return
operation == Operation::ADC || operation == Operation::AND ||
operation == Operation::CMP || operation == Operation::EOR ||
operation == Operation::LDA || operation == Operation::ORA ||
operation == Operation::SBC;
}
/*!
@returns The name of @c operation.
*/
inline constexpr const char *operation_name(Operation operation) {
#define MAP(x) case Operation::x: return #x;
switch(operation) {
default: break;
MAP(BBC0); MAP(BBC1); MAP(BBC2); MAP(BBC3); MAP(BBC4); MAP(BBC5); MAP(BBC6); MAP(BBC7);
MAP(BBS0); MAP(BBS1); MAP(BBS2); MAP(BBS3); MAP(BBS4); MAP(BBS5); MAP(BBS6); MAP(BBS7);
MAP(BCC); MAP(BCS); MAP(BEQ); MAP(BMI); MAP(BNE); MAP(BPL); MAP(BVC); MAP(BVS);
MAP(BRA); MAP(BRK); MAP(JMP); MAP(JSR); MAP(RTI); MAP(RTS); MAP(CLC); MAP(CLD);
MAP(CLI); MAP(CLT); MAP(CLV); MAP(SEC); MAP(SED); MAP(SEI); MAP(SET); MAP(INX);
MAP(INY); MAP(DEX); MAP(DEY); MAP(FST); MAP(SLW); MAP(NOP); MAP(PHA); MAP(PHP);
MAP(PLA); MAP(PLP); MAP(STP); MAP(TAX); MAP(TAY); MAP(TSX); MAP(TXA); MAP(TXS);
MAP(TYA); MAP(ADC); MAP(SBC); MAP(AND); MAP(ORA); MAP(EOR); MAP(BIT); MAP(CMP);
MAP(CPX); MAP(CPY); MAP(LDA); MAP(LDX); MAP(LDY); MAP(TST); MAP(ASL); MAP(LSR);
MAP(CLB0); MAP(CLB1); MAP(CLB2); MAP(CLB3); MAP(CLB4); MAP(CLB5); MAP(CLB6); MAP(CLB7);
MAP(SEB0); MAP(SEB1); MAP(SEB2); MAP(SEB3); MAP(SEB4); MAP(SEB5); MAP(SEB6); MAP(SEB7);
MAP(COM); MAP(DEC); MAP(INC); MAP(ROL); MAP(ROR); MAP(RRF); MAP(LDM); MAP(STA);
MAP(STX); MAP(STY);
}
#undef MAP
return "???";
}
inline std::ostream &operator <<(std::ostream &stream, Operation operation) {
stream << operation_name(operation);
return stream;
}
/*!
@returns The name of @c addressing_mode.
*/
inline constexpr const char *addressing_mode_name(AddressingMode addressing_mode) {
switch(addressing_mode) {
default: break;
case AddressingMode::Implied: return "";
case AddressingMode::Accumulator: return "A";
case AddressingMode::Immediate: return "#";
case AddressingMode::Absolute: return "abs";
case AddressingMode::AbsoluteX: return "abs, x";
case AddressingMode::AbsoluteY: return "abs, y";
case AddressingMode::ZeroPage: return "zp";
case AddressingMode::ZeroPageX: return "zp, x";
case AddressingMode::ZeroPageY: return "zp, y";
case AddressingMode::XIndirect: return "((zp, x))";
case AddressingMode::IndirectY: return "((zp), y)";
case AddressingMode::Relative: return "rel";
case AddressingMode::AbsoluteIndirect: return "(abs)";
case AddressingMode::ZeroPageIndirect: return "(zp)";
case AddressingMode::SpecialPage: return "\\sp";
case AddressingMode::ImmediateZeroPage: return "#, zp";
case AddressingMode::AccumulatorRelative: return "A, rel";
case AddressingMode::ZeroPageRelative: return "zp, rel";
}
return "???";
}
inline std::ostream &operator <<(std::ostream &stream, AddressingMode mode) {
stream << addressing_mode_name(mode);
return stream;
}
/*!
@returns The way that the address for an operation with @c addressing_mode and encoded starting from @c operation
would appear in an assembler. E.g. '$5a' for that zero page address, or '$5a, x' for zero-page indexed from $5a. This function
may access up to three bytes from @c operation onwards.
*/
inline std::string address(AddressingMode addressing_mode, const uint8_t *operation, uint16_t program_counter) {
std::stringstream output;
output << std::hex;
#define NUM(x) std::setfill('0') << std::setw(2) << int(x)
#define NUM4(x) std::setfill('0') << std::setw(4) << int(x)
switch(addressing_mode) {
default: return "???";
case AddressingMode::Implied: return "";
case AddressingMode::Accumulator: return "A ";
case AddressingMode::Immediate: output << "#$" << NUM(operation[1]); break;
case AddressingMode::Absolute: output << "$" << NUM(operation[2]) << NUM(operation[1]); break;
case AddressingMode::AbsoluteX: output << "$" << NUM(operation[2]) << NUM(operation[1]) << ", x"; break;
case AddressingMode::AbsoluteY: output << "$" << NUM(operation[2]) << NUM(operation[1]) << ", y"; break;
case AddressingMode::ZeroPage: output << "$" << NUM(operation[1]); break;
case AddressingMode::ZeroPageX: output << "$" << NUM(operation[1]) << ", x"; break;
case AddressingMode::ZeroPageY: output << "$" << NUM(operation[1]) << ", y"; break;
case AddressingMode::XIndirect: output << "(($" << NUM(operation[1]) << ", x))"; break;
case AddressingMode::IndirectY: output << "(($" << NUM(operation[1]) << "), y)"; break;
case AddressingMode::Relative: output << "#$" << NUM4(2 + program_counter + int8_t(operation[1])); break;
case AddressingMode::AbsoluteIndirect: output << "($" << NUM(operation[2]) << NUM(operation[1]) << ") "; break;
case AddressingMode::ZeroPageIndirect: output << "($" << NUM(operation[1]) << ")"; break;
case AddressingMode::SpecialPage: output << "$1f" << NUM(operation[1]); break;
case AddressingMode::ImmediateZeroPage: output << "#$" << NUM(operation[1]) << ", $" << NUM(operation[2]); break;
case AddressingMode::AccumulatorRelative: output << "A, $" << NUM4(2 + program_counter + int8_t(operation[1])); break;
case AddressingMode::ZeroPageRelative:
output << "$" << NUM(operation[1]) << ", $" << NUM4(3 + program_counter + int8_t(operation[2]));
break;
}
#undef NUM4
#undef NUM
return output.str();
}
/*!
Models a complete M50740-style instruction, including its operation, addressing mode and opcode.
*/
struct Instruction {
Operation operation = Operation::Invalid;
AddressingMode addressing_mode = AddressingMode::Implied;
uint8_t opcode = 0;
Instruction(Operation operation, AddressingMode addressing_mode, uint8_t opcode) : operation(operation), addressing_mode(addressing_mode), opcode(opcode) {}
Instruction(uint8_t opcode) : opcode(opcode) {}
Instruction() {}
};
/*!
Outputs a description of @c instruction to @c stream.
*/
inline std::ostream &operator <<(std::ostream &stream, const Instruction &instruction) {
stream << operation_name(instruction.operation) << " " << addressing_mode_name(instruction.addressing_mode);
return stream;
}
}
}
#endif /* InstructionSets_M50740_Instruction_h */

View File

@ -0,0 +1,125 @@
//
// Parser.hpp
// Clock Signal
//
// Created by Thomas Harte on 16/01/21.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef InstructionSets_M50740_Parser_hpp
#define InstructionSets_M50740_Parser_hpp
#include <cstdint>
#include "Decoder.hpp"
#include "../AccessType.hpp"
namespace InstructionSet {
namespace M50740 {
template<typename Target, bool include_entries_and_accesses> struct Parser {
void parse(Target &target, const uint8_t *storage, uint16_t start, uint16_t closing_bound) {
Decoder decoder;
while(start <= closing_bound) {
const auto next = decoder.decode(&storage[start], 1 + closing_bound - start);
if(next.first <= 0) {
// If there weren't enough bytes left before the closing bound to complete
// an instruction, but implicitly there were some bytes left, announce overflow
// and terminate.
target.announce_overflow(start);
return;
} else {
// Pass on the instruction.
target.announce_instruction(start, next.second);
if constexpr(!include_entries_and_accesses) {
// Do a simplified test: is this a terminating operation?
switch(next.second.operation) {
case Operation::RTS: case Operation::RTI: case Operation::BRK:
case Operation::JMP: case Operation::BRA:
return;
default: break;
}
} else {
// Check for end of stream and potential new entry points.
switch(next.second.operation) {
// Terminating instructions.
case Operation::RTS: case Operation::RTI: case Operation::BRK:
return;
// Terminating operations, possibly with implied additional entry point.
case Operation::JMP:
if(next.second.addressing_mode == AddressingMode::Absolute) {
target.add_entry(uint16_t(storage[start + 1] | (storage[start + 2] << 8)));
}
return;
case Operation::BRA:
target.add_entry(uint16_t(start + 2 + int8_t(storage[start + 1])));
return;
// Instructions that suggest another entry point but don't terminate parsing.
case Operation::BCC: case Operation::BCS:
case Operation::BVC: case Operation::BVS:
case Operation::BMI: case Operation::BPL:
case Operation::BNE: case Operation::BEQ:
target.add_entry(uint16_t(start + 2 + int8_t(storage[start + 1])));
break;
case Operation::JSR:
switch(next.second.addressing_mode) {
default: break;
case AddressingMode::Absolute:
target.add_entry(uint16_t(storage[start + 1] | (storage[start + 2] << 8)));
break;
case AddressingMode::SpecialPage:
target.add_entry(uint16_t(storage[start + 1] | 0x1f00));
break;
}
break;
case Operation::BBS0: case Operation::BBS1: case Operation::BBS2: case Operation::BBS3:
case Operation::BBS4: case Operation::BBS5: case Operation::BBS6: case Operation::BBS7:
case Operation::BBC0: case Operation::BBC1: case Operation::BBC2: case Operation::BBC3:
case Operation::BBC4: case Operation::BBC5: case Operation::BBC6: case Operation::BBC7:
switch(next.second.addressing_mode) {
default: break;
case AddressingMode::AccumulatorRelative:
target.add_entry(uint16_t(start + 2 + int8_t(storage[start + 1])));
break;
case AddressingMode::ZeroPageRelative:
target.add_entry(uint16_t(start + 3 + int8_t(storage[start + 2])));
break;
}
break;
default: break;
}
// Provide any fixed address accesses.
switch(next.second.addressing_mode) {
case AddressingMode::Absolute:
target.add_access(uint16_t(storage[start + 1] | (storage[start + 2] << 8)), access_type(next.second.operation));
break;
case AddressingMode::ZeroPage: case AddressingMode::ZeroPageRelative:
target.add_access(storage[start + 1], access_type(next.second.operation));
break;
case AddressingMode::ImmediateZeroPage:
target.add_access(storage[start + 2], access_type(next.second.operation));
break;
default: break;
}
}
// Advance.
start += next.first;
}
}
}
};
}
}
#endif /* InstructionSets_M50740_Parser_hpp */

View File

@ -2,13 +2,13 @@
// Decoder.cpp
// Clock Signal
//
// Created by Thomas Harte on 12/30/20.
// Created by Thomas Harte on 30/12/20.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#include "Decoder.hpp"
using namespace CPU::Decoder::PowerPC;
using namespace InstructionSet::PowerPC;
Decoder::Decoder(Model model) : model_(model) {}

View File

@ -2,7 +2,7 @@
// Decoder.hpp
// Clock Signal
//
// Created by Thomas Harte on 12/30/20.
// Created by Thomas Harte on 30/12/20.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
@ -11,8 +11,7 @@
#include "Instruction.hpp"
namespace CPU {
namespace Decoder {
namespace InstructionSet {
namespace PowerPC {
enum class Model {
@ -51,7 +50,6 @@ struct Decoder {
}
};
}
}
}

View File

@ -2,7 +2,7 @@
// Instruction.hpp
// Clock Signal
//
// Created by Thomas Harte on 1/15/21.
// Created by Thomas Harte on 15/01/21.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
@ -11,8 +11,7 @@
#include <cstdint>
namespace CPU {
namespace Decoder {
namespace InstructionSet {
namespace PowerPC {
enum class Operation: uint8_t {
@ -185,7 +184,6 @@ struct Instruction {
// Sanity check on Instruction size.
static_assert(sizeof(Instruction) <= 8);
}
}
}

View File

@ -1,5 +1,13 @@
# Instruction Sets
Code in here provides the means to disassemble, and to execute code for certain instruction sets.
It **does not seek to emulate specific processors** other than in terms of implementing their instruction sets. So:
* it doesn't involve itself in the actual bus signalling of real processors; and
* instruction-level timing (e.g. total cycle counts) may be unimplemented, and is likely to be incomplete.
This part of CLK is intended primarily to provide disassembly services for static analysis, and processing for machines where timing is not part of the specification — i.e. anything that's an instruction set and a HAL.
## Decoders
A decoder extracts fully-decoded instructions from a data stream for its associated architecture.
@ -21,11 +29,11 @@ Disassemblers are likely to decode an instruction, output it, and then immediate
Instruction executors may opt to cache decoded instructions to reduce recurrent costs, but will always be dealing with an actual instruction stream. The chance of caching means that decoded instructions should seek to be small. If helpful then a decoder might prefer to return a `std::pair` or similar of ephemeral information and stuff that it is meaningful to store.
## Likely Interfaces
### Likely Interfaces
These examples assume that the processor itself doesn't hold any state that affects instruction parsing. Whether processors with such state offer more than one decoder or take state as an argument will be a question of measure and effect.
### Fixed-size instruction words
#### Fixed-size instruction words
If the instructions are a fixed size, the decoder can provide what is functionally a simple lookup, whether implemented as such or not:
@ -33,7 +41,7 @@ If the instructions are a fixed size, the decoder can provide what is functional
For now I have preferred not to make this a simple constructor on `Instruction` because I'm reserving the option of switching to an ephemeral/permanent split in what's returned. More consideration needs to be applied here.
### Variable-size instruction words
#### Variable-size instruction words
If instructions are a variable size, the decoder should maintain internal state such that it can be provided with fragments of instructions until a full decoding has occurred — this avoids an assumption that all source bytes will always be laid out linearly in memory.
@ -47,8 +55,32 @@ In this sample the returned pair provides an `int` size that is one of:
A caller is permitted to react in any way it prefers to negative numbers; they're a hint potentially to reduce calling overhead only. A size of `0` would be taken to have the same meaning as a size of `-1`.
## Tying Decoders into Instruction Executors
## Parsers
It is assumed that disassemblers and bus-centric CPU emulators have limited generic functionality; for executors it is assumed that a processor-specific instruction fetcher and a dispatcher will be provided to couple with the decoder.
A parser sits one level above a decoder; it is handed:
* a start address;
* a closing bound; and
* a target.
Therefore decoders should adopt whatever interface is most natural; the expected uses information above is to provide a motivation for the scope of responsibilities and hints as to likely performance objectives only. Beyond requiring that decoded instructions be a tangible struct or class, it is not intended to be prescriptive as to form or interface.
It is responsible for parsing the instruction stream from the start address up to and not beyond the closing bound, and no further than any unconditional branches.
It should post to the target:
* any instructions fully decoded;
* any conditional branch destinations encountered;
* any immediately-knowable accessed addresses; and
* if a final instruction exists but runs beyond the closing bound, notification of that fact.
So a parser has the same two primary potential recipients as a decoder: diassemblers, and executors.
## Executors
An executor is responsible for only one thing:
* mapping from decoded instructions to objects that can perform those instructions.
An executor is assumed to bundle all the things that go into instruction set execution: processor state and memory, alongside a parser.
## Caching Executor
The caching executor is a generic class templated on a specific executor. It will use an executor to cache the results of parsing.
Idiomatically, the objects that perform instructions will expect to receive an appropriate executor as an argument. If they require other information, such as a copy of the decoded instruction, it should be built into the classes.

34
InstructionSets/Sizes.hpp Normal file
View File

@ -0,0 +1,34 @@
//
// Sizes.hpp
// Clock Signal
//
// Created by Thomas Harte on 26/01/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef Sizes_h
#define Sizes_h
/*!
Maps to the smallest of the following integers that can contain max_value:
* uint8_t;
* uint16_t;
* uint32_t; or
* uint64_t.
*/
template <uint64_t max_value> struct MinIntTypeValue {
using type =
std::conditional_t<
max_value <= std::numeric_limits<uint8_t>::max(), uint8_t,
std::conditional_t<
max_value <= std::numeric_limits<uint16_t>::max(), uint16_t,
std::conditional_t<
max_value <= std::numeric_limits<uint32_t>::max(), uint32_t,
uint64_t
>
>
>;
};
#endif /* Sizes_h */

View File

@ -2,7 +2,7 @@
// x86.cpp
// Clock Signal
//
// Created by Thomas Harte on 1/1/21.
// Created by Thomas Harte on 01/01/21.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
@ -12,12 +12,12 @@
#include <cassert>
#include <utility>
using namespace CPU::Decoder::x86;
using namespace InstructionSet::x86;
// Only 8086 is suppoted for now.
Decoder::Decoder(Model) {}
std::pair<int, CPU::Decoder::x86::Instruction> Decoder::decode(const uint8_t *source, size_t length) {
std::pair<int, InstructionSet::x86::Instruction> Decoder::decode(const uint8_t *source, size_t length) {
const uint8_t *const end = source + length;
// MARK: - Prefixes (if present) and the opcode.

View File

@ -2,7 +2,7 @@
// Decoder.hpp
// Clock Signal
//
// Created by Thomas Harte on 1/1/21.
// Created by Thomas Harte on 01/01/21.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
@ -11,10 +11,10 @@
#include "Instruction.hpp"
#include <cstddef>
#include <utility>
namespace CPU {
namespace Decoder {
namespace InstructionSet {
namespace x86 {
enum class Model {
@ -26,7 +26,7 @@ enum class Model {
This is an experimental implementation; it has not yet undergone significant testing.
*/
struct Decoder {
class Decoder {
public:
Decoder(Model model);
@ -149,7 +149,6 @@ struct Decoder {
}
};
}
}
}

View File

@ -2,7 +2,7 @@
// Instruction.hpp
// Clock Signal
//
// Created by Thomas Harte on 1/15/21.
// Created by Thomas Harte on 15/01/21.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
@ -11,8 +11,7 @@
#include <cstdint>
namespace CPU {
namespace Decoder {
namespace InstructionSet {
namespace x86 {
/*
@ -298,7 +297,6 @@ class Instruction {
static_assert(sizeof(Instruction) <= 8);
}
}
}

113
Machines/Apple/ADB/Bus.cpp Normal file
View File

@ -0,0 +1,113 @@
//
// Bus.cpp
// Clock Signal
//
// Created by Thomas Harte on 10/02/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Bus.hpp"
using namespace Apple::ADB;
Bus::Bus(HalfCycles clock_speed) : half_cycles_to_microseconds_(1'000'000.0 / clock_speed.as<double>()) {}
void Bus::run_for(HalfCycles duration) {
time_in_state_ += duration;
time_since_get_state_ += duration;
}
void Bus::set_device_output(size_t device_id, bool output) {
// Modify the all-devices bus state.
bus_state_[device_id] = output;
// React to signal edges only; don't use get_state here to avoid
// endless recursion should any reactive devices set new output
// during the various calls made below.
const bool data_level = bus_state_.all();
if(data_level_ != data_level) {
data_level_ = data_level;
if(data_level) {
// This was a transition to high; classify what just happened according to
// the duration of the low period.
const double low_microseconds = time_in_state_.as<double>() * half_cycles_to_microseconds_;
// Low periods:
// (partly as adapted from the AN591 data sheet; otherwise from the IIgs reference manual)
//
// > 1040 µs reset
// 5601040 µs attention
// < 50 µs 1
// 5072 µs 0
// 300 µs service request
if(low_microseconds > 1040.0) {
for(auto device: devices_) {
device->adb_bus_did_observe_event(Event::Reset);
}
} else if(low_microseconds >= 560.0) {
for(auto device: devices_) {
device->adb_bus_did_observe_event(Event::Attention);
}
shift_register_ = 1;
start_target_ = 9; // Consume the stop bit before posting the next byte.
phase_ = Phase::AttentionCapture;
} else if(low_microseconds < 50.0) {
shift(1);
} else if(low_microseconds < 72.0) {
shift(0);
} else if(low_microseconds >= 291.0 && low_microseconds <= 309.0) {
for(auto device: devices_) {
device->adb_bus_did_observe_event(Event::ServiceRequest);
}
} else {
for(auto device: devices_) {
device->adb_bus_did_observe_event(Event::Unrecognised);
}
}
}
time_in_state_ = HalfCycles(0);
}
}
void Bus::shift(unsigned int value) {
shift_register_ = (shift_register_ << 1) | value;
// Trigger a byte whenever either:
// * a 'start bit' hits bit 8; or
// * if this was a command byte, wait for the stop bit (i.e. the start bit hits 9).
if(shift_register_ & (1 << start_target_)) {
for(auto device: devices_) {
device->adb_bus_did_observe_event(Event::Byte, uint8_t(shift_register_ >> (start_target_ - 8)));
}
// Expect a real start bit only if moving from attention capture to packet
// capture. Otherwise adopt an implied start bit.
shift_register_ = phase_ == Phase::PacketCapture;
start_target_ = 8;
phase_ = Phase::PacketCapture;
}
}
bool Bus::get_state() const {
const auto microseconds = time_since_get_state_.as<double>() * half_cycles_to_microseconds_;
time_since_get_state_ = HalfCycles(0);
const bool current_level = bus_state_.all();
for(auto device: devices_) {
device->advance_state(microseconds, current_level);
}
return bus_state_.all();
}
size_t Bus::add_device() {
const size_t id = next_device_id_;
++next_device_id_;
return id;
}
size_t Bus::add_device(Device *device) {
devices_.push_back(device);
return add_device();
}

172
Machines/Apple/ADB/Bus.hpp Normal file
View File

@ -0,0 +1,172 @@
//
// Bus.hpp
// Clock Signal
//
// Created by Thomas Harte on 10/02/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef Bus_hpp
#define Bus_hpp
#include "../../../ClockReceiver/ClockReceiver.hpp"
#include <bitset>
#include <cstddef>
#include <ostream>
#include <vector>
namespace Apple {
namespace ADB {
struct Command {
enum class Type {
Reset,
Flush,
Reserved,
/// The host wishes the device to store register contents.
Listen,
/// The host wishes the device to broadcast register contents.
Talk
};
static constexpr uint8_t AllDevices = 0xff;
static constexpr uint8_t NoRegister = 0xff;
Type type = Type::Reserved;
uint8_t device = AllDevices;
uint8_t reg = NoRegister;
Command() {}
Command(Type type) : type(type) {}
Command(Type type, uint8_t device) : type(type), device(device) {}
Command(Type type, uint8_t device, uint8_t reg) : type(type), device(device), reg(reg) {}
};
inline std::ostream &operator <<(std::ostream &stream, Command::Type type) {
switch(type) {
case Command::Type::Reset: stream << "reset"; break;
case Command::Type::Flush: stream << "flush"; break;
case Command::Type::Listen: stream << "listen"; break;
case Command::Type::Talk: stream << "talk"; break;
default: stream << "reserved"; break;
}
return stream;
}
inline std::ostream &operator <<(std::ostream &stream, Command command) {
stream << "Command {";
if(command.device != 0xff) stream << "device " << int(command.device) << ", ";
if(command.reg != 0xff) stream << "register " << int(command.reg) << ", ";
stream << command.type;
stream << "}";
return stream;
}
/*!
@returns The @c Command encoded in @c code.
*/
inline Command decode_command(uint8_t code) {
switch(code & 0x0f) {
default: return Command();
case 0: return Command(Command::Type::Reset);
case 1: return Command(Command::Type::Flush, code >> 4);
case 8: case 9: case 10: case 11:
return Command(Command::Type::Listen, code >> 4, code & 3);
case 12: case 13: case 14: case 15:
return Command(Command::Type::Talk, code >> 4, code & 3);
}
}
/*!
The ADB bus models the data line of the ADB bus; it allows multiple devices to
post their current data level, or read the current level, and also offers a tokenised
version of all activity on the bus.
In implementation terms, two types of device are envisaged:
* proactive devices, which use @c add_device() and then merely @c set_device_output
and @c get_state() as required, according to their own tracking of time; and
* reactive devices, which use @c add_device(Device*) and then merely react to
@c adb_bus_did_observe_event and @c advance_state in order to
update @c set_device_output.
*/
class Bus {
public:
Bus(HalfCycles clock_speed);
/*!
Advances time; ADB is a clocked serial signal.
*/
void run_for(HalfCycles);
/*!
Adds a device to the bus, returning the index it should use
to refer to itself in subsequent calls to set_device_output.
*/
size_t add_device();
/*!
Sets the current data line output for @c device.
*/
void set_device_output(size_t device_id, bool output);
/*!
@returns The current state of the ADB data line.
*/
bool get_state() const;
enum class Event {
Reset,
Attention,
Byte,
ServiceRequest,
Unrecognised
};
struct Device {
/// Reports to an observer that @c event was observed in the activity
/// observed on this bus. If this was a byte event, that byte's value is given as @c value.
virtual void adb_bus_did_observe_event(Event event, uint8_t value = 0xff) = 0;
/// Requests that the device update itself @c microseconds and, if necessary, post a
/// new value ot @c set_device_output. This will be called only when the bus needs
/// to reevaluate its current level. It cannot reliably be used to track the timing between
/// observed events.
virtual void advance_state(double microseconds, bool current_level) = 0;
};
/*!
Adds a device.
*/
size_t add_device(Device *);
private:
HalfCycles time_in_state_;
mutable HalfCycles time_since_get_state_;
double half_cycles_to_microseconds_ = 1.0;
std::vector<Device *> devices_;
unsigned int shift_register_ = 0;
unsigned int start_target_ = 8;
bool data_level_ = true;
// ADB addressing supports at most 16 devices but that doesn't include
// the controller. So assume a maximum of 17 connected devices.
std::bitset<17> bus_state_{0xffffffff};
size_t next_device_id_ = 0;
inline void shift(unsigned int);
enum class Phase {
PacketCapture,
AttentionCapture
} phase_ = Phase::AttentionCapture;
};
}
}
#endif /* Bus_hpp */

View File

@ -0,0 +1,242 @@
//
// Keyboard.cpp
// Clock Signal
//
// Created by Thomas Harte on 13/02/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Keyboard.hpp"
using namespace Apple::ADB;
Keyboard::Keyboard(Bus &bus) : ReactiveDevice(bus, 2) {}
void Keyboard::perform_command(const Command &command) {
switch(command.type) {
case Command::Type::Reset:
modifiers_ = 0xffff;
case Command::Type::Flush: {
std::lock_guard lock_guard(keys_mutex_);
pending_events_.clear();
} break;
case Command::Type::Talk:
switch(command.reg) {
case 0: {
// Post up to two key events, or nothing if there are
// no events pending.
std::lock_guard lock_guard(keys_mutex_);
if(!pending_events_.empty()) {
if(pending_events_.size() > 1) {
post_response({pending_events_[0], pending_events_[1]});
pending_events_.erase(pending_events_.begin(), pending_events_.begin()+2);
} else {
// Two bytes are required; provide a key up of the fictional
// key zero as the second.
// That's arbitrary; verify with real machines.
post_response({pending_events_[0], 0x80});
pending_events_.clear();
}
}
} break;
case 2: {
std::lock_guard lock_guard(keys_mutex_);
post_response({uint8_t(modifiers_ >> 8), uint8_t(modifiers_)});
} break;
default: break;
}
break;
case Command::Type::Listen:
// If a listen is incoming for register 2, prepare to capture LED statuses.
if(command.reg == 2) {
receive_bytes(2);
}
break;
default: break;
}
}
void Keyboard::did_receive_data(const Command &, const std::vector<uint8_t> &data) {
// This must be a register 2 listen; update the LED statuses.
// TODO: and possibly display these.
modifiers_ = (modifiers_ & 0xfff8) | (data[1] & 7);
}
bool Keyboard::set_key_pressed(Key key, bool is_pressed) {
// ADB keyboard events: low 7 bits are a key code; bit 7 is either 0 for pressed or 1 for released.
std::lock_guard lock_guard(keys_mutex_);
pending_events_.push_back(uint8_t(key) | (is_pressed ? 0x00 : 0x80));
pressed_keys_[size_t(key)] = is_pressed;
// Track modifier state also.
/*
In all cases below: 0 = pressed/on; 1 = released/off.
b15: None (reserved)
b14: Delete
b13: Caps lock
b12: Reset
b11: Control
b10: Shift
b9: Option
b8: Command
-- From here onwards, available only on the extended keyboard.
b7: Num lock/clear
b6: Scroll lock
b53: None (reserved)
b2: Scroll Lock LED
b1: Caps Lock LED
b0: Num Lock LED
*/
#define SetModifierBit(x) modifiers_ = (modifiers_ & ~x) | (is_pressed ? 0 : x);
#define ToggleModifierBit(x) if(is_pressed) modifiers_ ^= x;
switch(key) {
default: break;
case Key::Delete: SetModifierBit(0x4000); break;
case Key::CapsLock: ToggleModifierBit(0x2000); break;
case Key::Power: SetModifierBit(0x1000); break;
case Key::LeftControl:
case Key::RightControl:
SetModifierBit(0x0800);
break;
case Key::LeftShift:
case Key::RightShift:
SetModifierBit(0x0400);
break;
case Key::LeftOption:
case Key::RightOption:
SetModifierBit(0x0200);
break;
case Key::Command:
SetModifierBit(0x0100);
break;
case Key::KeypadClear: ToggleModifierBit(0x0080); break;
case Key::Help: ToggleModifierBit(0x0040); break;
}
#undef SetModifierBit
// Ensure service occurs.
post_service_request();
return true;
}
void Keyboard::clear_all_keys() {
// For all keys currently marked as down, enqueue key-up actions.
std::lock_guard lock_guard(keys_mutex_);
for(size_t key = 0; key < pressed_keys_.size(); key++) {
if(pressed_keys_[key]) {
pending_events_.push_back(0x80 | uint8_t(key));
pressed_keys_[key] = false;
}
}
if(!pending_events_.empty()) post_service_request();
// Mark all modifiers as released.
modifiers_ |= 0xfff8;
}
// MARK: - KeyboardMapper
uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const {
using Key = Inputs::Keyboard::Key;
using ADBKey = Apple::ADB::Key;
switch(key) {
default: return MachineTypes::MappedKeyboardMachine::KeyNotMapped;
#define Bind(x, y) case Key::x: return uint16_t(ADBKey::y)
#define BindDirect(x) Bind(x, x)
BindDirect(BackTick);
BindDirect(k1); BindDirect(k2); BindDirect(k3); BindDirect(k4); BindDirect(k5);
BindDirect(k6); BindDirect(k7); BindDirect(k8); BindDirect(k9); BindDirect(k0);
BindDirect(Help);
BindDirect(Home);
BindDirect(PageUp);
BindDirect(Delete);
BindDirect(End);
BindDirect(PageDown);
BindDirect(Escape);
BindDirect(Hyphen);
BindDirect(Equals);
BindDirect(Backspace);
BindDirect(Tab);
BindDirect(F1); BindDirect(F2); BindDirect(F3); BindDirect(F4);
BindDirect(F5); BindDirect(F6); BindDirect(F7); BindDirect(F8);
BindDirect(F9); BindDirect(F10); BindDirect(F11); BindDirect(F12);
BindDirect(Q); BindDirect(W); BindDirect(E); BindDirect(R);
BindDirect(T); BindDirect(Y); BindDirect(U); BindDirect(I);
BindDirect(O); BindDirect(P); BindDirect(A); BindDirect(S);
BindDirect(D); BindDirect(F); BindDirect(G); BindDirect(H);
BindDirect(J); BindDirect(K); BindDirect(L); BindDirect(Z);
BindDirect(X); BindDirect(C); BindDirect(V); BindDirect(B);
BindDirect(N); BindDirect(M);
BindDirect(OpenSquareBracket);
BindDirect(CloseSquareBracket);
BindDirect(Semicolon);
BindDirect(Quote);
BindDirect(Comma);
BindDirect(FullStop);
BindDirect(ForwardSlash);
BindDirect(CapsLock);
BindDirect(LeftShift); BindDirect(RightShift);
BindDirect(LeftControl); BindDirect(RightControl);
BindDirect(LeftOption); BindDirect(RightOption);
Bind(LeftMeta, Command); Bind(RightMeta, Command);
BindDirect(Space);
BindDirect(Backslash);
Bind(Enter, Return);
BindDirect(Left); BindDirect(Right);
BindDirect(Up); BindDirect(Down);
Bind(KeypadDelete, KeypadClear);
BindDirect(KeypadEquals);
BindDirect(KeypadSlash);
BindDirect(KeypadAsterisk);
BindDirect(KeypadMinus);
BindDirect(KeypadPlus);
BindDirect(KeypadEnter);
BindDirect(KeypadDecimalPoint);
BindDirect(Keypad9);
BindDirect(Keypad8);
BindDirect(Keypad7);
BindDirect(Keypad6);
BindDirect(Keypad5);
BindDirect(Keypad4);
BindDirect(Keypad3);
BindDirect(Keypad2);
BindDirect(Keypad1);
BindDirect(Keypad0);
// Leaving unmapped:
// Power, F13, F14, F15
#undef BindDirect
#undef Bind
}
}

View File

@ -0,0 +1,125 @@
//
// Keyboard.hpp
// Clock Signal
//
// Created by Thomas Harte on 13/02/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef Keyboard_hpp
#define Keyboard_hpp
#include "ReactiveDevice.hpp"
#include "../../../Inputs/Keyboard.hpp"
#include "../../KeyboardMachine.hpp"
#include <array>
#include <cstdint>
#include <mutex>
#include <vector>
namespace Apple {
namespace ADB {
/*!
Defines the keycodes that could be passed directly via set_key_pressed; these
are based on the Apple Extended Keyboard.
*/
enum class Key: uint16_t {
/*
These are transcribed from Page 19-11 of
the Macintosh Family Hardware Reference.
*/
BackTick = 0x32,
k1 = 0x12, k2 = 0x13, k3 = 0x14, k4 = 0x15, k5 = 0x17,
k6 = 0x16, k7 = 0x1a, k8 = 0x1c, k9 = 0x19, k0 = 0x1d,
Help = 0x72,
Home = 0x73,
PageUp = 0x74,
Delete = 0x75,
End = 0x77,
PageDown = 0x79,
Escape = 0x35,
Hyphen = 0x1b,
Equals = 0x18,
Backspace = 0x33,
Tab = 0x30,
Power = 0x7f,
F1 = 0x7a, F2 = 0x78, F3 = 0x63, F4 = 0x76,
F5 = 0x60, F6 = 0x61, F7 = 0x62, F8 = 0x64,
F9 = 0x65, F10 = 0x6d, F11 = 0x67, F12 = 0x6f,
F13 = 0x69, F14 = 0x6b, F15 = 0x71,
Q = 0x0c, W = 0x0d, E = 0x0e, R = 0x0f, T = 0x11, Y = 0x10, U = 0x20, I = 0x22, O = 0x1f, P = 0x23,
A = 0x00, S = 0x01, D = 0x02, F = 0x03, G = 0x05, H = 0x04, J = 0x26, K = 0x28, L = 0x25,
Z = 0x06, X = 0x07, C = 0x08, V = 0x09, B = 0x0b, N = 0x2d, M = 0x2e,
OpenSquareBracket = 0x21,
CloseSquareBracket = 0x1e,
Semicolon = 0x29,
Quote = 0x27,
Comma = 0x2b,
FullStop = 0x2f,
ForwardSlash = 0x2c,
CapsLock = 0x39,
LeftShift = 0x38, RightShift = 0x7b,
LeftControl = 0x36, RightControl = 0x7d,
LeftOption = 0x3a, RightOption = 0x7c,
Command = 0x37,
Space = 0x31,
Backslash = 0x2a,
Return = 0x24,
Left = 0x3b,
Right = 0x3c,
Up = 0x3e,
Down = 0x3d,
KeypadClear = 0x47,
KeypadEquals = 0x51,
KeypadSlash = 0x4b,
KeypadAsterisk = 0x43,
KeypadMinus = 0x4e,
KeypadPlus = 0x45,
KeypadEnter = 0x4c,
KeypadDecimalPoint = 0x41,
Keypad9 = 0x5c, Keypad8 = 0x5b, Keypad7 = 0x59,
Keypad6 = 0x58, Keypad5 = 0x57, Keypad4 = 0x56,
Keypad3 = 0x55, Keypad2 = 0x54, Keypad1 = 0x53,
Keypad0 = 0x52,
};
class Keyboard: public ReactiveDevice {
public:
Keyboard(Bus &);
bool set_key_pressed(Key key, bool is_pressed);
void clear_all_keys();
private:
void perform_command(const Command &command) override;
void did_receive_data(const Command &, const std::vector<uint8_t> &) override;
std::mutex keys_mutex_;
std::array<bool, 128> pressed_keys_;
std::vector<uint8_t> pending_events_;
uint16_t modifiers_ = 0xffff;
};
/*!
Provides a mapping from idiomatic PC keys to ADB keys.
*/
class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const final;
};
}
}
#endif /* Keyboard_hpp */

View File

@ -0,0 +1,62 @@
//
// Mouse.cpp
// Clock Signal
//
// Created by Thomas Harte on 12/02/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Mouse.hpp"
using namespace Apple::ADB;
Mouse::Mouse(Bus &bus) : ReactiveDevice(bus, 3) {}
void Mouse::perform_command(const Command &command) {
if(command.type == Command::Type::Talk && command.reg == 0) {
// Read current deltas and buttons, thread safely.
auto delta_x = delta_x_.exchange(0);
auto delta_y = delta_y_.exchange(0);
const int buttons = button_flags_;
// Clamp deltas.
delta_x = std::max(std::min(delta_x, int16_t(127)), int16_t(-128));
delta_y = std::max(std::min(delta_y, int16_t(127)), int16_t(-128));
// Figure out what that would look like, and don't respond if there's
// no change to report.
const uint16_t reg0 =
((buttons & 1) ? 0x0000 : 0x8000) |
((buttons & 2) ? 0x0000 : 0x0080) |
uint16_t(delta_x & 0x7f) |
uint16_t((delta_y & 0x7f) << 8);
if(reg0 == last_posted_reg0_) return;
// Post change.
last_posted_reg0_ = reg0;
post_response({uint8_t(reg0 >> 8), uint8_t(reg0)});
}
}
void Mouse::move(int x, int y) {
delta_x_ += int16_t(x);
delta_y_ += int16_t(y);
post_service_request();
}
int Mouse::get_number_of_buttons() {
return 2;
}
void Mouse::set_button_pressed(int index, bool is_pressed) {
if(is_pressed)
button_flags_ |= (1 << index);
else
button_flags_ &= ~(1 << index);
post_service_request();
}
void Mouse::reset_all_buttons() {
button_flags_ = 0;
post_service_request();
}

View File

@ -0,0 +1,38 @@
//
// Mouse.hpp
// Clock Signal
//
// Created by Thomas Harte on 12/02/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef Mouse_hpp
#define Mouse_hpp
#include "ReactiveDevice.hpp"
#include "../../../Inputs/Mouse.hpp"
namespace Apple {
namespace ADB {
class Mouse: public ReactiveDevice, public Inputs::Mouse {
public:
Mouse(Bus &);
private:
void perform_command(const Command &command) override;
void move(int x, int y) override;
int get_number_of_buttons() override;
void set_button_pressed(int index, bool is_pressed) override;
void reset_all_buttons() override;
std::atomic<int16_t> delta_x_, delta_y_;
std::atomic<int> button_flags_ = 0;
uint16_t last_posted_reg0_ = 0;
};
}
}
#endif /* Mouse_hpp */

View File

@ -0,0 +1,180 @@
//
// ReactiveDevice.cpp
// Clock Signal
//
// Created by Thomas Harte on 12/02/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "ReactiveDevice.hpp"
#define LOG_PREFIX "[ADB device] "
#include "../../../Outputs/Log.hpp"
using namespace Apple::ADB;
ReactiveDevice::ReactiveDevice(Apple::ADB::Bus &bus, uint8_t adb_device_id) :
bus_(bus),
device_id_(bus.add_device(this)),
default_adb_device_id_(adb_device_id) {
reset();
}
void ReactiveDevice::post_response(const std::vector<uint8_t> &&response) {
response_ = std::move(response);
microseconds_at_bit_ = 0.0;
bit_offset_ = -2;
}
void ReactiveDevice::advance_state(double microseconds, bool current_level) {
// First test: is a service request desired?
if(phase_ == Phase::ServiceRequestPending) {
microseconds_at_bit_ += microseconds;
if(microseconds_at_bit_ < 240.0) {
bus_.set_device_output(device_id_, false);
} else {
bus_.set_device_output(device_id_, true);
phase_ = Phase::AwaitingAttention;
}
return;
}
// Do nothing if not in the process of posting a response.
if(response_.empty()) return;
/*
Total process below:
(1) assume that the data was enqueued before the stop bit had
concluded; wait for the end of that;
(2) wait for the stop-to-start time period;
(3) output a start bit of '1';
(4) output all enqueued bytes, MSB to LSB;
(5) output a stop bit of '0'; and
(6) return this device's output level to high and top.
*/
// Wait for the bus to be clear if transmission has not yet begun.
if(!current_level && bit_offset_ == -2) return;
// Advance time.
microseconds_at_bit_ += microseconds;
// If this is the start of the packet, wait an appropriate stop-to-start time.
if(bit_offset_ == -2) {
if(microseconds_at_bit_ < 150.0) {
return;
}
microseconds_at_bit_ -= 150.0;
++bit_offset_;
}
// Advance the implied number of bits.
const int step = int(microseconds_at_bit_ / 100.0);
bit_offset_ += step;
microseconds_at_bit_ -= double(step * 100.0);
// Check for end-of-transmission.
const int response_bit_length = int(response_.size() * 8);
if(bit_offset_ >= 1 + response_bit_length) {
bus_.set_device_output(device_id_, true);
response_.clear();
return;
}
// Otherwise pick the bit to output: it'll either be the start bit of 1,
// from the provided data, or a stop bit of 0.
int bit = 0;
if(bit_offset_ < 0) {
bit = 1;
} else if(bit_offset_ < response_bit_length) {
const int byte = bit_offset_ >> 3;
const int packet = int(response_[size_t(byte)]);
bit = (packet >> (7 - (bit_offset_ & 7))) & 1;
}
// Convert that into a level.
constexpr double low_periods[] = {66, 33};
bus_.set_device_output(device_id_, microseconds_at_bit_ > low_periods[bit]);
}
void ReactiveDevice::adb_bus_did_observe_event(Bus::Event event, uint8_t value) {
if(phase_ == Phase::AwaitingAttention) {
if(event != Bus::Event::Attention) return;
phase_ = Phase::AwaitingCommand;
return;
}
if(event != Bus::Event::Byte) return;
if(phase_ == Phase::AwaitingContent) {
content_.push_back(value);
if(content_.size() == expected_content_size_) {
phase_ = Phase::AwaitingAttention;
if(command_.reg == 3) {
register3_ = uint16_t((content_[0] << 8) | content_[1]);
} else {
did_receive_data(command_, content_);
}
content_.clear();
}
}
if(phase_ == Phase::AwaitingCommand) {
phase_ = Phase::AwaitingAttention;
command_ = decode_command(value);
// LOG(command_);
// If this command doesn't apply here, but a service request is requested,
// post a service request.
if(command_.device != Command::AllDevices && command_.device != ((register3_ >> 8) & 0xf)) {
if(service_desired_) {
service_desired_ = false;
stop_has_begin_ = false;
phase_ = Phase::ServiceRequestPending;
microseconds_at_bit_ = 0.0;
}
return;
}
// Handle reset and register 3 here automatically; pass everything else along.
switch(command_.type) {
case Command::Type::Reset:
reset();
[[fallthrough]];
default:
perform_command(command_);
break;
case Command::Type::Listen:
case Command::Type::Talk:
if(command_.reg == 3) {
if(command_.type == Command::Type::Talk) {
post_response({uint8_t(register3_ >> 8), uint8_t(register3_ & 0xff)});
} else {
receive_bytes(2);
}
} else {
service_desired_ = false;
perform_command(command_);
}
break;
}
}
}
void ReactiveDevice::receive_bytes(size_t count) {
content_.clear();
expected_content_size_ = count;
phase_ = Phase::AwaitingContent;
}
void ReactiveDevice::reset() {
register3_ = uint16_t(0x6001 | (default_adb_device_id_ << 8));
}
void ReactiveDevice::post_service_request() {
service_desired_ = true;
}

View File

@ -0,0 +1,66 @@
//
// ReactiveDevice.hpp
// Clock Signal
//
// Created by Thomas Harte on 12/02/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef ReactiveDevice_hpp
#define ReactiveDevice_hpp
#include "Bus.hpp"
#include <atomic>
#include <cstddef>
#include <vector>
namespace Apple {
namespace ADB {
class ReactiveDevice: public Bus::Device {
protected:
ReactiveDevice(Bus &bus, uint8_t adb_device_id);
void post_response(const std::vector<uint8_t> &&response);
void post_service_request();
void receive_bytes(size_t count);
virtual void perform_command(const Command &command) = 0;
virtual void did_receive_data(const Command &, const std::vector<uint8_t> &) {}
private:
void advance_state(double microseconds, bool current_level) override;
void adb_bus_did_observe_event(Bus::Event event, uint8_t value) override;
private:
Bus &bus_;
const size_t device_id_;
std::vector<uint8_t> response_;
int bit_offset_ = 0;
double microseconds_at_bit_ = 0;
enum class Phase {
AwaitingAttention,
AwaitingCommand,
AwaitingContent,
ServiceRequestPending,
} phase_ = Phase::AwaitingAttention;
std::vector<uint8_t> content_;
size_t expected_content_size_ = 0;
Command command_;
bool stop_has_begin_ = false;
uint16_t register3_;
const uint8_t default_adb_device_id_;
std::atomic<bool> service_desired_ = false;
void reset();
};
}
}
#endif /* ReactiveDevice_hpp */

View File

@ -19,8 +19,11 @@
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "../../../Outputs/Log.hpp"
#include "AuxiliaryMemorySwitches.hpp"
#include "Card.hpp"
#include "DiskIICard.hpp"
#include "Joystick.hpp"
#include "LanguageCardSwitches.hpp"
#include "Video.hpp"
#include "../../../Analyser/Static/AppleII/Target.hpp"
@ -37,6 +40,7 @@ namespace II {
#define is_iie() ((model == Analyser::Static::AppleII::Target::Model::IIe) || (model == Analyser::Static::AppleII::Target::Model::EnhancedIIe))
template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
public Apple::II::Machine,
public MachineTypes::TimedMachine,
public MachineTypes::ScanProducer,
public MachineTypes::AudioProducer,
@ -46,7 +50,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
public CPU::MOS6502::BusHandler,
public Inputs::Keyboard,
public Configurable::Device,
public Apple::II::Machine,
public Activity::Source,
public Apple::II::Card::Delegate {
private:
@ -175,7 +178,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
to paging every 6502 page of memory independently. It makes the paging events more expensive,
but hopefully more clear.
*/
uint8_t *read_pages_[256]; // each is a pointer to the 256-block of memory the CPU should read when accessing that page of memory
const uint8_t *read_pages_[256]; // each is a pointer to the 256-block of memory the CPU should read when accessing that page of memory
uint8_t *write_pages_[256]; // as per read_pages_, but this is where the CPU should write. If a pointer is nullptr, don't write.
void page(int start, int end, uint8_t *read, uint8_t *write) {
for(int position = start; position < end; ++position) {
@ -187,128 +190,79 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
}
}
// MARK: - The language card.
struct {
bool bank1 = false;
bool read = false;
bool pre_write = false;
bool write = false;
} language_card_;
bool has_language_card_ = true;
// MARK: The language card.
LanguageCardSwitches<ConcreteMachine> language_card_;
AuxiliaryMemorySwitches<ConcreteMachine> auxiliary_switches_;
friend LanguageCardSwitches<ConcreteMachine>;
friend AuxiliaryMemorySwitches<ConcreteMachine>;
void set_language_card_paging() {
uint8_t *const ram = alternative_zero_page_ ? aux_ram_ : ram_;
const auto language_state = language_card_.state();
const auto zero_state = auxiliary_switches_.zero_state();
uint8_t *const ram = zero_state ? aux_ram_ : ram_;
uint8_t *const rom = is_iie() ? &rom_[3840] : rom_.data();
// Which way the region here is mapped to be banks 1 and 2 is
// arbitrary.
page(0xd0, 0xe0,
language_card_.read ? &ram[language_card_.bank1 ? 0xd000 : 0xc000] : rom,
language_card_.write ? nullptr : &ram[language_card_.bank1 ? 0xd000 : 0xc000]);
language_state.read ? &ram[language_state.bank2 ? 0xd000 : 0xc000] : rom,
language_state.write ? nullptr : &ram[language_state.bank2 ? 0xd000 : 0xc000]);
page(0xe0, 0x100,
language_card_.read ? &ram[0xe000] : &rom[0x1000],
language_card_.write ? nullptr : &ram[0xe000]);
language_state.read ? &ram[0xe000] : &rom[0x1000],
language_state.write ? nullptr : &ram[0xe000]);
}
// MARK - The IIe's ROM controls.
bool internal_CX_rom_ = false;
bool slot_C3_rom_ = false;
bool internal_c8_rom_ = false;
// MARK: Auxiliary memory and the other IIe improvements.
void set_card_paging() {
page(0xc1, 0xc8, internal_CX_rom_ ? rom_.data() : nullptr, nullptr);
const auto state = auxiliary_switches_.card_state();
if(!internal_CX_rom_) {
if(!slot_C3_rom_) read_pages_[0xc3] = &rom_[0xc300 - 0xc100];
}
page(0xc8, 0xd0, (internal_CX_rom_ || internal_c8_rom_) ? &rom_[0xc800 - 0xc100] : nullptr, nullptr);
page(0xc1, 0xc4, state.region_C1_C3 ? &rom_[0xc100 - 0xc100] : nullptr, nullptr);
read_pages_[0xc3] = state.region_C3 ? &rom_[0xc300 - 0xc100] : nullptr;
page(0xc4, 0xc8, state.region_C4_C8 ? &rom_[0xc400 - 0xc100] : nullptr, nullptr);
page(0xc8, 0xd0, state.region_C8_D0 ? &rom_[0xc800 - 0xc100] : nullptr, nullptr);
}
// MARK - The IIe's auxiliary RAM controls.
bool alternative_zero_page_ = false;
void set_zero_page_paging() {
if(alternative_zero_page_) {
read_pages_[0] = aux_ram_;
if(auxiliary_switches_.zero_state()) {
write_pages_[0] = aux_ram_;
} else {
read_pages_[0] = ram_;
write_pages_[0] = ram_;
}
read_pages_[1] = read_pages_[0] + 256;
write_pages_[0] = read_pages_[0];
write_pages_[1] = read_pages_[1];
write_pages_[1] = write_pages_[0] + 256;
read_pages_[0] = write_pages_[0];
read_pages_[1] = write_pages_[1];
// Zero page banking also affects interpretation of the language card's switches.
set_language_card_paging();
}
bool read_auxiliary_memory_ = false;
bool write_auxiliary_memory_ = false;
void set_main_paging() {
page(0x02, 0xc0,
read_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200],
write_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200]);
const auto state = auxiliary_switches_.main_state();
if(video_.get_80_store()) {
bool use_aux_ram = video_.get_page2();
page(0x04, 0x08,
use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400],
use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400]);
page(0x02, 0x04,
state.base.read ? &aux_ram_[0x0200] : &ram_[0x0200],
state.base.write ? &aux_ram_[0x0200] : &ram_[0x0200]);
page(0x08, 0x20,
state.base.read ? &aux_ram_[0x0800] : &ram_[0x0800],
state.base.write ? &aux_ram_[0x0800] : &ram_[0x0800]);
page(0x40, 0xc0,
state.base.read ? &aux_ram_[0x4000] : &ram_[0x4000],
state.base.write ? &aux_ram_[0x4000] : &ram_[0x4000]);
if(video_.get_high_resolution()) {
page(0x20, 0x40,
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000],
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000]);
}
}
page(0x04, 0x08,
state.region_04_08.read ? &aux_ram_[0x0400] : &ram_[0x0400],
state.region_04_08.write ? &aux_ram_[0x0400] : &ram_[0x0400]);
page(0x20, 0x40,
state.region_20_40.read ? &aux_ram_[0x2000] : &ram_[0x2000],
state.region_20_40.write ? &aux_ram_[0x2000] : &ram_[0x2000]);
}
// MARK - typing
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
// MARK - joysticks
class Joystick: public Inputs::ConcreteJoystick {
public:
Joystick() :
ConcreteJoystick({
Input(Input::Horizontal),
Input(Input::Vertical),
// The Apple II offers three buttons between two joysticks;
// this emulator puts three buttons on each joystick and
// combines them.
Input(Input::Fire, 0),
Input(Input::Fire, 1),
Input(Input::Fire, 2),
}) {}
void did_set_input(const Input &input, float value) final {
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical))
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
}
void did_set_input(const Input &input, bool value) final {
if(input.type == Input::Type::Fire && input.info.control.index < 3) {
buttons[input.info.control.index] = value;
}
}
bool buttons[3] = {false, false, false};
float axes[2] = {0.5f, 0.5f};
};
// On an Apple II, the programmer strobes 0xc070 and that causes each analogue input
// to begin a charge and discharge cycle **if they are not already charging**.
// The greater the analogue input, the faster they will charge and therefore the sooner
// they will discharge.
//
// This emulator models that with analogue_charge_ being essentially the amount of time,
// in charge threshold units, since 0xc070 was last strobed. But if any of the analogue
// inputs were already partially charged then they gain a bias in analogue_biases_.
//
// It's a little indirect, but it means only having to increment the one value in the
// main loop.
float analogue_charge_ = 0.0f;
float analogue_biases_[4] = {0.0f, 0.0f, 0.0f, 0.0f};
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
bool analogue_channel_is_discharged(size_t channel) {
return (1.0f - static_cast<Joystick *>(joysticks_[channel >> 1].get())->axes[channel & 1]) < analogue_charge_ + analogue_biases_[channel];
}
// MARK - Joysticks.
JoystickPair joysticks_;
// The IIe has three keys that are wired directly to the same input as the joystick buttons.
bool open_apple_is_pressed_ = false;
@ -320,7 +274,9 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
video_bus_handler_(ram_, aux_ram_),
video_(video_bus_handler_),
audio_toggle_(audio_queue_),
speaker_(audio_toggle_) {
speaker_(audio_toggle_),
language_card_(*this),
auxiliary_switches_(*this) {
// The system's master clock rate.
constexpr float master_clock = 14318180.0;
@ -342,10 +298,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
Memory::Fuzz(ram_, sizeof(ram_));
Memory::Fuzz(aux_ram_, sizeof(aux_ram_));
// Add a couple of joysticks.
joysticks_.emplace_back(new Joystick);
joysticks_.emplace_back(new Joystick);
// Pick the required ROMs.
using Target = Analyser::Static::AppleII::Target;
const std::string machine_name = "AppleII";
@ -353,21 +305,21 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
size_t rom_size = 12*1024;
switch(target.model) {
default:
rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::II));
rom_descriptions.emplace_back(machine_name, "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588);
break;
case Target::Model::IIplus:
rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::II));
rom_descriptions.emplace_back(machine_name, "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26);
break;
case Target::Model::IIe:
rom_size += 3840;
rom_descriptions.emplace_back(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1);
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::IIe));
rom_descriptions.emplace_back(machine_name, "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18d);
break;
case Target::Model::EnhancedIIe:
rom_size += 3840;
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d);
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::EnhancedIIe));
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942);
break;
}
@ -400,9 +352,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
// Set the whole card area to initially backed by nothing.
page(0xc0, 0xd0, nullptr, nullptr);
// Set proper values for the language card/ROM area.
set_language_card_paging();
insert_media(target.media);
}
@ -457,14 +406,8 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
if(write_pages_[address >> 8]) write_pages_[address >> 8][address & 0xff] = *value;
}
if(is_iie() && address >= 0xc300 && address < 0xd000) {
bool internal_c8_rom = internal_c8_rom_;
internal_c8_rom |= ((address >> 8) == 0xc3) && !slot_C3_rom_;
internal_c8_rom &= (address != 0xcfff);
if(internal_c8_rom != internal_c8_rom_) {
internal_c8_rom_ = internal_c8_rom;
set_card_paging();
}
if(is_iie()) {
auxiliary_switches_.access(address, isReadOperation(operation));
}
} else {
// Assume a vapour read unless it turns out otherwise; this is a little
@ -502,7 +445,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc061: // Switch input 0.
*value &= 0x7f;
if(
static_cast<Joystick *>(joysticks_[0].get())->buttons[0] || static_cast<Joystick *>(joysticks_[1].get())->buttons[2] ||
joysticks_.button(0) ||
(is_iie() && open_apple_is_pressed_)
)
*value |= 0x80;
@ -510,14 +453,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc062: // Switch input 1.
*value &= 0x7f;
if(
static_cast<Joystick *>(joysticks_[0].get())->buttons[1] || static_cast<Joystick *>(joysticks_[1].get())->buttons[1] ||
joysticks_.button(1) ||
(is_iie() && closed_apple_is_pressed_)
)
*value |= 0x80;
break;
case 0xc063: // Switch input 2.
*value &= 0x7f;
if(static_cast<Joystick *>(joysticks_[0].get())->buttons[2] || static_cast<Joystick *>(joysticks_[1].get())->buttons[0])
if(joysticks_.button(2))
*value |= 0x80;
break;
@ -527,20 +470,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc067: { // Analogue input 3.
const size_t input = address - 0xc064;
*value &= 0x7f;
if(!analogue_channel_is_discharged(input)) {
if(!joysticks_.analogue_channel_is_discharged(input)) {
*value |= 0x80;
}
} break;
// The IIe-only state reads follow...
#define IIeSwitchRead(s) *value = get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00);
case 0xc011: IIeSwitchRead(language_card_.bank1); break;
case 0xc012: IIeSwitchRead(language_card_.read); break;
case 0xc013: IIeSwitchRead(read_auxiliary_memory_); break;
case 0xc014: IIeSwitchRead(write_auxiliary_memory_); break;
case 0xc015: IIeSwitchRead(internal_CX_rom_); break;
case 0xc016: IIeSwitchRead(alternative_zero_page_); break;
case 0xc017: IIeSwitchRead(slot_C3_rom_); break;
case 0xc011: IIeSwitchRead(language_card_.state().bank2); break;
case 0xc012: IIeSwitchRead(language_card_.state().read); break;
case 0xc013: IIeSwitchRead(auxiliary_switches_.switches().read_auxiliary_memory); break;
case 0xc014: IIeSwitchRead(auxiliary_switches_.switches().write_auxiliary_memory); break;
case 0xc015: IIeSwitchRead(auxiliary_switches_.switches().internal_CX_rom); break;
case 0xc016: IIeSwitchRead(auxiliary_switches_.switches().alternative_zero_page); break;
case 0xc017: IIeSwitchRead(auxiliary_switches_.switches().slot_C3_rom); break;
case 0xc018: IIeSwitchRead(video_.get_80_store()); break;
case 0xc019: IIeSwitchRead(video_.get_is_vertical_blank(cycles_since_video_update_)); break;
case 0xc01a: IIeSwitchRead(video_.get_text()); break;
@ -558,6 +501,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
} else {
// Write-only switches. All IIe as currently implemented.
if(is_iie()) {
auxiliary_switches_.access(address, false);
switch(address) {
default: break;
@ -565,40 +509,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc001:
update_video();
video_.set_80_store(!!(address&1));
set_main_paging();
break;
case 0xc002:
case 0xc003:
read_auxiliary_memory_ = !!(address&1);
set_main_paging();
break;
case 0xc004:
case 0xc005:
write_auxiliary_memory_ = !!(address&1);
set_main_paging();
break;
case 0xc006:
case 0xc007:
internal_CX_rom_ = !!(address&1);
set_card_paging();
break;
case 0xc008:
case 0xc009:
// The alternative zero page setting affects both bank 0 and any RAM
// that's paged as though it were on a language card.
alternative_zero_page_ = !!(address&1);
set_zero_page_paging();
set_language_card_paging();
break;
case 0xc00a:
case 0xc00b:
slot_C3_rom_ = !!(address&1);
set_card_paging();
break;
case 0xc00c:
@ -617,17 +527,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
}
break;
case 0xc070: { // Permit analogue inputs that are currently discharged to begin a charge cycle.
// Ensure those that were still charging retain that state.
for(size_t c = 0; c < 4; ++c) {
if(analogue_channel_is_discharged(c)) {
analogue_biases_[c] = 0.0f;
} else {
analogue_biases_[c] += analogue_charge_;
}
}
analogue_charge_ = 0.0f;
} break;
case 0xc070: joysticks_.access_c070(); break;
/* Switches triggered by reading or writing. */
case 0xc050:
@ -640,14 +540,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc054:
case 0xc055:
update_video();
video_.set_page2(!!(address&1));
set_main_paging();
video_.set_page2(address&1);
auxiliary_switches_.access(address, isReadOperation(operation));
break;
case 0xc056:
case 0xc057:
update_video();
video_.set_high_resolution(!!(address&1));
set_main_paging();
video_.set_high_resolution(address&1);
auxiliary_switches_.access(address, isReadOperation(operation));
break;
case 0xc05e:
@ -681,28 +581,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc081: case 0xc085: case 0xc089: case 0xc08d:
case 0xc082: case 0xc086: case 0xc08a: case 0xc08e:
case 0xc083: case 0xc087: case 0xc08b: case 0xc08f:
// Quotes below taken from Understanding the Apple II, p. 5-28 and 5-29.
// "A3 controls the 4K bank selection"
language_card_.bank1 = (address&8);
// "Access to $C080, $C083, $C084, $0087, $C088, $C08B, $C08C, or $C08F sets the READ ENABLE flip-flop"
// (other accesses reset it)
language_card_.read = !(((address&2) >> 1) ^ (address&1));
// "The WRITE ENABLE' flip-flop is reset by an odd read access to the $C08X range when the PRE-WRITE flip-flop is set."
if(language_card_.pre_write && isReadOperation(operation) && (address&1)) language_card_.write = false;
// "[The WRITE ENABLE' flip-flop] is set by an even access in the $C08X range."
if(!(address&1)) language_card_.write = true;
// ("Any other type of access causes the WRITE ENABLE' flip-flop to hold its current state.")
// "The PRE-WRITE flip-flop is set by an odd read access in the $C08X range. It is reset by an even access or a write access."
language_card_.pre_write = isReadOperation(operation) ? (address&1) : false;
// Apply whatever the net effect of all that is to the memory map.
set_language_card_paging();
language_card_.access(address, isReadOperation(operation));
break;
}
@ -788,7 +667,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
}
// Update analogue charge level.
analogue_charge_ = std::min(analogue_charge_ + 1.0f / 2820.0f, 1.1f);
joysticks_.update_charge();
return Cycles(1);
}
@ -916,7 +795,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
// MARK: JoystickMachine
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return joysticks_;
return joysticks_.get_joysticks();
}
};

View File

@ -0,0 +1,275 @@
//
// AuxiliaryMemorySwitches.hpp
// Clock Signal
//
// Created by Thomas Harte on 22/10/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef AuxiliaryMemorySwitches_h
#define AuxiliaryMemorySwitches_h
namespace Apple {
namespace II {
/*!
Models the auxiliary memory soft switches, added as of the Apple IIe, which allow access to the auxiliary 64kb of RAM and to
the additional almost-4kb of ROM.
Relevant memory accesses should be fed to this class; it'll call:
* machine.set_main_paging() if anything in the 'main' state changes, i.e. the lower 48kb excluding the zero and stack pages;
* machine.set_card_state() if anything changes with where ROM should appear rather than cards in the $Cxxx range; and
* machine.set_zero_page_paging() if the selection of the lowest two pages of RAM changes.
Implementation observation: as implemented on the IIe, the zero page setting also affects what happens in the language card area.
*/
template <typename Machine> class AuxiliaryMemorySwitches {
public:
static constexpr bool Auxiliary = true;
static constexpr bool Main = false;
static constexpr bool ROM = true;
static constexpr bool Card = false;
/// Describes banking state between $0200 and $BFFF.
struct MainState {
struct Region {
/// @c true indicates auxiliary memory should be read from; @c false indicates main.
bool read = false;
/// @c true indicates auxiliary memory should be written to; @c false indicates main.
bool write = false;
};
/// Describes banking state in the ranges $0200$03FF, $0800$1FFF and $4000$BFFF.
Region base;
/// Describes banking state in the range $0400$07FF.
Region region_04_08;
/// Describes banking state in the range $2000$3FFF.
Region region_20_40;
bool operator != (const MainState &rhs) const {
return
base.read != rhs.base.read || base.write != rhs.base.write ||
region_04_08.read != rhs.region_04_08.read || region_04_08.write != rhs.region_04_08.write ||
region_20_40.read != rhs.region_20_40.read || region_20_40.write != rhs.region_20_40.write;
}
};
/// Describes banking state between $C100 and $Cfff.
struct CardState {
/// @c true indicates that the built-in ROM should appear from $C100 to $C2FF @c false indicates that cards should service those accesses.
bool region_C1_C3 = false;
/// @c true indicates that the built-in ROM should appear from $C300 to $C3FF; @c false indicates that cards should service those accesses.
bool region_C3 = false;
/// @c true indicates that the built-in ROM should appear from $C400 to $C7FF; @c false indicates that cards should service those accesses.
bool region_C4_C8 = false;
/// @c true indicates that the built-in ROM should appear from $C800 to $CFFF; @c false indicates that cards should service those accesses.
bool region_C8_D0 = false;
bool operator != (const CardState &rhs) const {
return
region_C1_C3 != rhs.region_C1_C3 ||
region_C3 != rhs.region_C3 ||
region_C4_C8 != rhs.region_C4_C8 ||
region_C8_D0 != rhs.region_C8_D0;
}
};
/// Descibes banking state between $0000 and $01ff; @c true indicates that auxiliary memory should be used; @c false indicates main memory.
using ZeroState = bool;
/// Returns raw switch state for all switches that affect banking, even if they're logically video switches.
struct SwitchState {
bool read_auxiliary_memory = false;
bool write_auxiliary_memory = false;
bool internal_CX_rom = false;
bool slot_C3_rom = false;
bool internal_C8_rom = false;
bool store_80 = false;
bool alternative_zero_page = false;
bool video_page_2 = false;
bool high_resolution = false;
};
AuxiliaryMemorySwitches(Machine &machine) : machine_(machine) {}
/// Used by an owner to forward, at least, any access in the range $C000 to $C00B,
/// in $C054 to $C058, or in the range $C300 to $CFFF. Safe to call for any [16-bit] address.
void access(uint16_t address, bool is_read) {
if(address >= 0xc300 && address < 0xd000) {
switches_.internal_C8_rom |= ((address >> 8) == 0xc3) && !switches_.slot_C3_rom;
switches_.internal_C8_rom &= (address != 0xcfff);
set_card_paging();
return;
}
if(address < 0xc000 || address >= 0xc058) return;
switch(address) {
default: break;
case 0xc000: case 0xc001:
if(!is_read) {
switches_.store_80 = address & 1;
set_main_paging();
}
break;
case 0xc002: case 0xc003:
if(!is_read) {
switches_.read_auxiliary_memory = address & 1;
set_main_paging();
}
break;
case 0xc004: case 0xc005:
if(!is_read) {
switches_.write_auxiliary_memory = address & 1;
set_main_paging();
}
break;
case 0xc006: case 0xc007:
if(!is_read) {
switches_.internal_CX_rom = address & 1;
set_card_paging();
}
break;
case 0xc008: case 0xc009: {
const bool alternative_zero_page = address & 1;
if(!is_read && switches_.alternative_zero_page != alternative_zero_page) {
switches_.alternative_zero_page = alternative_zero_page;
set_zero_page_paging();
}
} break;
case 0xc00a: case 0xc00b:
if(!is_read) {
switches_.slot_C3_rom = address & 1;
set_card_paging();
}
break;
case 0xc054: case 0xc055:
switches_.video_page_2 = address & 1;
set_main_paging();
break;
case 0xc056: case 0xc057:
switches_.high_resolution = address & 1;
set_main_paging();
break;
}
}
/// Provides part of the IIgs interface.
void set_state(uint8_t value) {
// b7: 1 => use auxiliary memory for zero page; 0 => use main. [I think the Hardware Reference gets this the wrong way around]
// b6: 1 => text page 2 is selected; 0 => text page 1.
// b5: 1 => auxiliary RAM bank is selected for reads; 0 => main.
// b4: 1 => auxiliary RAM bank is selected for writes; 0 => main.
// b0: 1 => the internal ROM is selected for C800+; 0 => card ROM.
switches_.alternative_zero_page = value & 0x80;
switches_.video_page_2 = value & 0x40;
switches_.read_auxiliary_memory = value & 0x20;
switches_.write_auxiliary_memory = value & 0x10;
switches_.internal_CX_rom = value & 0x01;
set_main_paging();
set_zero_page_paging();
set_card_paging();
}
uint8_t get_state() const {
return
(switches_.alternative_zero_page ? 0x80 : 0x00) |
(switches_.video_page_2 ? 0x40 : 0x00) |
(switches_.read_auxiliary_memory ? 0x20 : 0x00) |
(switches_.write_auxiliary_memory ? 0x10 : 0x00) |
(switches_.internal_CX_rom ? 0x01 : 0x00);
}
const MainState &main_state() const {
return main_state_;
}
const CardState &card_state() const {
return card_state_;
}
/// @returns @c true if the alternative zero page should be used; @c false otherwise.
const ZeroState zero_state() const {
return switches_.alternative_zero_page;
}
const SwitchState switches() const {
return switches_;
}
private:
Machine &machine_;
SwitchState switches_;
MainState main_state_;
void set_main_paging() {
const auto previous_state = main_state_;
// The two appropriately named switches provide the base case.
main_state_.base.read = switches_.read_auxiliary_memory;
main_state_.base.write = switches_.write_auxiliary_memory;
if(switches_.store_80) {
// If store 80 is set, use the page 2 flag for the lower carve out;
// if both store 80 and high resolution are set, use the page 2 flag for both carve outs.
main_state_.region_04_08.read = main_state_.region_04_08.write = switches_.video_page_2;
if(switches_.high_resolution) {
main_state_.region_20_40.read = main_state_.region_20_40.write = switches_.video_page_2;
} else {
main_state_.region_20_40 = main_state_.base;
}
} else {
main_state_.region_04_08 = main_state_.region_20_40 = main_state_.base;
}
if(previous_state != main_state_) {
machine_.set_main_paging();
}
}
CardState card_state_;
void set_card_paging() {
const auto previous_state = card_state_;
// By default apply the CX switch through to $C7FF.
card_state_.region_C1_C3 = card_state_.region_C4_C8 = switches_.internal_CX_rom;
// Allow the C3 region to be switched to internal ROM in isolation even if the rest of the
// first half of the CX region is diabled, if its specific switch is also disabled.
if(!switches_.internal_CX_rom && !switches_.slot_C3_rom) {
card_state_.region_C3 = true;
} else {
card_state_.region_C3 = card_state_.region_C1_C3;
}
// Apply the CX switch to $C800+, but also allow the C8 switch to select that region in isolation.
card_state_.region_C8_D0 = switches_.internal_CX_rom || switches_.internal_C8_rom;
if(previous_state != card_state_) {
machine_.set_card_paging();
}
}
void set_zero_page_paging() {
// Believe it or not, the zero page is just set or cleared by a single flag.
// As though life were rational.
machine_.set_zero_page_paging();
}
};
}
}
#endif /* AuxiliaryMemorySwitches_h */

View File

@ -0,0 +1,9 @@
//
// Joystick.cpp
// Clock Signal
//
// Created by Thomas Harte on 16/02/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Joystick.hpp"

View File

@ -0,0 +1,112 @@
//
// Joystick.hpp
// Clock Signal
//
// Created by Thomas Harte on 16/02/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef AppleII_Joystick_hpp
#define AppleII_Joystick_hpp
#include "../../../Inputs/Joystick.hpp"
#include <memory>
#include <vector>
namespace Apple {
namespace II {
class JoystickPair {
public:
JoystickPair() {
// Add a couple of joysticks.
joysticks_.emplace_back(new Joystick);
joysticks_.emplace_back(new Joystick);
}
class Joystick: public Inputs::ConcreteJoystick {
public:
Joystick() :
ConcreteJoystick({
Input(Input::Horizontal),
Input(Input::Vertical),
// The Apple II offers three buttons between two joysticks;
// this emulator puts three buttons on each joystick and
// combines them.
Input(Input::Fire, 0),
Input(Input::Fire, 1),
Input(Input::Fire, 2),
}) {}
void did_set_input(const Input &input, float value) final {
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical))
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
}
void did_set_input(const Input &input, bool value) final {
if(input.type == Input::Type::Fire && input.info.control.index < 3) {
buttons[input.info.control.index] = value;
}
}
bool buttons[3] = {false, false, false};
float axes[2] = {0.5f, 0.5f};
};
inline bool button(size_t index) {
return joystick(0)->buttons[index] || joystick(1)->buttons[2-index];
}
inline bool analogue_channel_is_discharged(size_t channel) {
return (1.0f - static_cast<Joystick *>(joysticks_[channel >> 1].get())->axes[channel & 1]) < analogue_charge_ + analogue_biases_[channel];
}
inline void update_charge(float one_mhz_cycles = 1.0f) {
analogue_charge_ = std::min(analogue_charge_ + one_mhz_cycles * (1.0f / 2820.0f), 1.1f);
}
inline void access_c070() {
// Permit analogue inputs that are currently discharged to begin a charge cycle.
// Ensure those that were still charging retain that state.
for(size_t c = 0; c < 4; ++c) {
if(analogue_channel_is_discharged(c)) {
analogue_biases_[c] = 0.0f;
} else {
analogue_biases_[c] += analogue_charge_;
}
}
analogue_charge_ = 0.0f;
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
return joysticks_;
}
private:
// On an Apple II, the programmer strobes 0xc070 and that causes each analogue input
// to begin a charge and discharge cycle **if they are not already charging**.
// The greater the analogue input, the faster they will charge and therefore the sooner
// they will discharge.
//
// This emulator models that with analogue_charge_ being essentially the amount of time,
// in charge threshold units, since 0xc070 was last strobed. But if any of the analogue
// inputs were already partially charged then they gain a bias in analogue_biases_.
//
// It's a little indirect, but it means only having to increment the one value in the
// main loop.
float analogue_charge_ = 0.0f;
float analogue_biases_[4] = {0.0f, 0.0f, 0.0f, 0.0f};
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
inline Joystick *joystick(size_t index) {
return static_cast<Joystick *>(joysticks_[index].get());
}
};
}
}
#endif /* AppleII_Joystick_hpp */

View File

@ -0,0 +1,116 @@
//
// LanguageCardSwitches.hpp
// Clock Signal
//
// Created by Thomas Harte on 22/10/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef LanguageCardSwitches_h
#define LanguageCardSwitches_h
namespace Apple {
namespace II {
/*!
Models the language card soft switches, present on any Apple II with a language card and provided built-in from the IIe onwards.
Relevant memory accesses should be fed to this class; it'll call:
* machine.set_language_card_paging() if the proper mapped state changes.
*/
template <typename Machine> class LanguageCardSwitches {
public:
struct State {
/// When RAM is visible in the range $D000$FFFF:
/// @c true indicates that bank 2 should be used between $D000 and $DFFF;
/// @c false indicates bank 1.
bool bank2 = true;
/// @c true indicates that RAM should be readable in the range $D000$FFFF;
/// @c false indicates ROM should be readable.
bool read = false;
/// @c true indicates that ROM is selected for 'writing' in the range $D000$FFFF (i.e. writes are a no-op);
/// @c false indicates that RAM is selected for writing.
bool write = false;
bool operator != (const State &rhs) const {
return
bank2 != rhs.bank2 ||
read != rhs.read ||
write != rhs.write;
}
};
LanguageCardSwitches(Machine &machine) : machine_(machine) {}
/// Used by an owner to forward any access to $c08x.
void access(uint16_t address, bool is_read) {
const auto previous_state = state_;
// Quotes below taken from Understanding the Apple II, p. 5-28 and 5-29.
// "A3 controls the 4K bank selection"; 0 = bank 2, 1 = bank 1.
state_.bank2 = !(address & 8);
// "Access to $C080, $C083, $C084, $0087, $C088, $C08B, $C08C, or $C08F sets the READ ENABLE flip-flop"
// (other accesses reset it)
state_.read = !(((address&2) >> 1) ^ (address&1));
// "The WRITE ENABLE' flip-flop is reset by an odd read access to the $C08X range when the PRE-WRITE flip-flop is set."
if(pre_write_ && is_read && (address&1)) state_.write = false;
// "[The WRITE ENABLE' flip-flop] is set by an even access in the $C08X range."
if(!(address&1)) state_.write = true;
// ("Any other type of access causes the WRITE ENABLE' flip-flop to hold its current state.")
// "The PRE-WRITE flip-flop is set by an odd read access in the $C08X range. It is reset by an even access or a write access."
pre_write_ = is_read ? (address&1) : false;
// Apply whatever the net effect of all that is to the memory map.
if(previous_state != state_) {
machine_.set_language_card_paging();
}
}
/// Provides read-only access to the current language card switch state.
const State &state() const {
return state_;
}
/// Provides relevant parts of the IIgs interface.
void set_state(uint8_t value) {
const auto previous_state = state_;
// Bit 3: 1 => enable ROM, 0 => enable RAM.
state_.read = !(value & 0x08);
// Bit 2: 1 => select bank 2, 0 => select bank 1. [per errata to the Hardware Reference
// correcting the original, which lists them the other way around]
state_.bank2 = value & 0x04;
if(previous_state != state_) {
machine_.set_language_card_paging();
}
}
uint8_t get_state() const {
return
(state_.read ? 0x00 : 0x08) |
(state_.bank2 ? 0x04 : 0x00);
}
private:
Machine &machine_;
State state_;
// This is an additional flip flop contained on the language card, but
// it is one step removed from current banking state, so I've excluded it
// from the State struct.
bool pre_write_ = false;
};
}
}
#endif /* LanguageCard_h */

View File

@ -11,9 +11,9 @@
using namespace Apple::II::Video;
VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
VideoSwitches<Cycles>(is_iie, Cycles(2), std::move(target)),
crt_(910, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance1),
is_iie_(is_iie),
deferrer_(std::move(target)) {
is_iie_(is_iie) {
// Show only the centre 75% of the TV frame.
crt_.set_display_type(Outputs::Display::DisplayType::CompositeColour);
@ -24,23 +24,6 @@ VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
// use default_colour_bursts elsewhere, though it otherwise should be. If/when
// it is, start doing so and return to setting the immediate phase up here.
// crt_.set_immediate_default_phase(0.5f);
character_zones[0].xor_mask = 0;
character_zones[0].address_mask = 0x3f;
character_zones[1].xor_mask = 0;
character_zones[1].address_mask = 0x3f;
character_zones[2].xor_mask = 0;
character_zones[2].address_mask = 0x3f;
character_zones[3].xor_mask = 0;
character_zones[3].address_mask = 0x3f;
if(is_iie) {
character_zones[0].xor_mask =
character_zones[2].xor_mask =
character_zones[3].xor_mask = 0xff;
character_zones[2].address_mask =
character_zones[3].address_mask = 0xff;
}
}
void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
@ -59,121 +42,10 @@ Outputs::Display::DisplayType VideoBase::get_display_type() const {
return crt_.get_display_type();
}
/*
Rote setters and getters.
*/
void VideoBase::set_alternative_character_set(bool alternative_character_set) {
set_alternative_character_set_ = alternative_character_set;
deferrer_.defer(Cycles(2), [this, alternative_character_set] {
alternative_character_set_ = alternative_character_set;
if(alternative_character_set) {
character_zones[1].address_mask = 0xff;
character_zones[1].xor_mask = 0;
} else {
character_zones[1].address_mask = 0x3f;
character_zones[1].xor_mask = flash_mask();
}
});
}
bool VideoBase::get_alternative_character_set() {
return set_alternative_character_set_;
}
void VideoBase::set_80_columns(bool columns_80) {
set_columns_80_ = columns_80;
deferrer_.defer(Cycles(2), [this, columns_80] {
columns_80_ = columns_80;
});
}
bool VideoBase::get_80_columns() {
return set_columns_80_;
}
void VideoBase::set_80_store(bool store_80) {
set_store_80_ = store_80_ = store_80;
}
bool VideoBase::get_80_store() {
return set_store_80_;
}
void VideoBase::set_page2(bool page2) {
set_page2_ = page2_ = page2;
}
bool VideoBase::get_page2() {
return set_page2_;
}
void VideoBase::set_text(bool text) {
set_text_ = text;
deferrer_.defer(Cycles(2), [this, text] {
text_ = text;
});
}
bool VideoBase::get_text() {
return set_text_;
}
void VideoBase::set_mixed(bool mixed) {
set_mixed_ = mixed;
deferrer_.defer(Cycles(2), [this, mixed] {
mixed_ = mixed;
});
}
bool VideoBase::get_mixed() {
return set_mixed_;
}
void VideoBase::set_high_resolution(bool high_resolution) {
set_high_resolution_ = high_resolution;
deferrer_.defer(Cycles(2), [this, high_resolution] {
high_resolution_ = high_resolution;
});
}
bool VideoBase::get_high_resolution() {
return set_high_resolution_;
}
void VideoBase::set_annunciator_3(bool annunciator_3) {
set_annunciator_3_ = annunciator_3;
deferrer_.defer(Cycles(2), [this, annunciator_3] {
annunciator_3_ = annunciator_3;
high_resolution_mask_ = annunciator_3_ ? 0x7f : 0xff;
});
}
bool VideoBase::get_annunciator_3() {
return set_annunciator_3_;
}
void VideoBase::set_character_rom(const std::vector<uint8_t> &character_rom) {
character_rom_ = character_rom;
// Flip all character contents based on the second line of the $ graphic.
if(character_rom_[0x121] == 0x3c || character_rom_[0x122] == 0x3c) {
for(auto &graphic : character_rom_) {
graphic =
((graphic & 0x01) ? 0x40 : 0x00) |
((graphic & 0x02) ? 0x20 : 0x00) |
((graphic & 0x04) ? 0x10 : 0x00) |
((graphic & 0x08) ? 0x08 : 0x00) |
((graphic & 0x10) ? 0x04 : 0x00) |
((graphic & 0x20) ? 0x02 : 0x00) |
((graphic & 0x40) ? 0x01 : 0x00);
}
}
}
void VideoBase::output_text(uint8_t *target, const uint8_t *const source, size_t length, size_t pixel_row) const {
for(size_t c = 0; c < length; ++c) {
const int character = source[c] & character_zones[source[c] >> 6].address_mask;
const uint8_t xor_mask = character_zones[source[c] >> 6].xor_mask;
const int character = source[c] & character_zones_[source[c] >> 6].address_mask;
const uint8_t xor_mask = character_zones_[source[c] >> 6].xor_mask;
const std::size_t character_address = size_t(character << 3) + pixel_row;
const uint8_t character_pattern = character_rom_[character_address] ^ xor_mask;
@ -194,19 +66,19 @@ void VideoBase::output_double_text(uint8_t *target, const uint8_t *const source,
for(size_t c = 0; c < length; ++c) {
const std::size_t character_addresses[2] = {
size_t(
(auxiliary_source[c] & character_zones[auxiliary_source[c] >> 6].address_mask) << 3
(auxiliary_source[c] & character_zones_[auxiliary_source[c] >> 6].address_mask) << 3
) + pixel_row,
size_t(
(source[c] & character_zones[source[c] >> 6].address_mask) << 3
(source[c] & character_zones_[source[c] >> 6].address_mask) << 3
) + pixel_row
};
const uint8_t character_patterns[2] = {
uint8_t(
character_rom_[character_addresses[0]] ^ character_zones[auxiliary_source[c] >> 6].xor_mask
character_rom_[character_addresses[0]] ^ character_zones_[auxiliary_source[c] >> 6].xor_mask
),
uint8_t(
character_rom_[character_addresses[1]] ^ character_zones[source[c] >> 6].xor_mask
character_rom_[character_addresses[1]] ^ character_zones_[source[c] >> 6].xor_mask
)
};
@ -270,26 +142,26 @@ void VideoBase::output_double_low_resolution(uint8_t *target, const uint8_t *con
const int row_shift = row&4;
for(size_t c = 0; c < length; ++c) {
if((column + int(c))&1) {
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 2;
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 4;
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 8;
target[3] = (auxiliary_source[c] >> row_shift) & 1;
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 4;
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 8;
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 1;
target[3] = (auxiliary_source[c] >> row_shift) & 2;
target[8] = target[12] = (source[c] >> row_shift) & 4;
target[9] = target[13] = (source[c] >> row_shift) & 8;
target[10] = (source[c] >> row_shift) & 1;
target[7] = target[11] = (source[c] >> row_shift) & 2;
target[8] = target[12] = (source[c] >> row_shift) & 8;
target[9] = target[13] = (source[c] >> row_shift) & 1;
target[10] = (source[c] >> row_shift) & 2;
target[7] = target[11] = (source[c] >> row_shift) & 4;
graphics_carry_ = (source[c] >> row_shift) & 8;
} else {
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 8;
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 1;
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 2;
target[3] = (auxiliary_source[c] >> row_shift) & 4;
target[0] = target[4] = (auxiliary_source[c] >> row_shift) & 1;
target[1] = target[5] = (auxiliary_source[c] >> row_shift) & 2;
target[2] = target[6] = (auxiliary_source[c] >> row_shift) & 4;
target[3] = (auxiliary_source[c] >> row_shift) & 8;
target[8] = target[12] = (source[c] >> row_shift) & 1;
target[9] = target[13] = (source[c] >> row_shift) & 2;
target[10] = (source[c] >> row_shift) & 4;
target[7] = target[11] = (source[c] >> row_shift) & 8;
target[8] = target[12] = (source[c] >> row_shift) & 2;
target[9] = target[13] = (source[c] >> row_shift) & 4;
target[10] = (source[c] >> row_shift) & 8;
target[7] = target[11] = (source[c] >> row_shift) & 1;
graphics_carry_ = (source[c] >> row_shift) & 2;
}
target += 14;

View File

@ -6,13 +6,15 @@
// Copyright 2018 Thomas Harte. All rights reserved.
//
#ifndef Video_hpp
#define Video_hpp
#ifndef Apple_II_Video_hpp
#define Apple_II_Video_hpp
#include "../../../Outputs/CRT/CRT.hpp"
#include "../../../ClockReceiver/ClockReceiver.hpp"
#include "../../../ClockReceiver/DeferredQueue.hpp"
#include "VideoSwitches.hpp"
#include <array>
#include <vector>
@ -33,7 +35,7 @@ class BusHandler {
}
};
class VideoBase {
class VideoBase: public VideoSwitches<Cycles> {
public:
VideoBase(bool is_iie, std::function<void(Cycles)> &&target);
@ -49,112 +51,6 @@ class VideoBase {
/// Gets the type of output.
Outputs::Display::DisplayType get_display_type() const;
/*
Descriptions for the setters below are taken verbatim from
the Apple IIe Technical Reference. Addresses are the conventional
locations within the Apple II memory map. Only those which affect
video output are implemented here.
Those registers which don't exist on a II/II+ are marked.
*/
/*!
Setter for ALTCHAR ($C00E/$C00F; triggers on write only):
* Off: display text using primary character set.
* On: display text using alternate character set.
Doesn't exist on a II/II+.
*/
void set_alternative_character_set(bool);
bool get_alternative_character_set();
/*!
Setter for 80COL ($C00C/$C00D; triggers on write only).
* Off: display 40 columns.
* On: display 80 columns.
Doesn't exist on a II/II+.
*/
void set_80_columns(bool);
bool get_80_columns();
/*!
Setter for 80STORE ($C000/$C001; triggers on write only).
* Off: cause PAGE2 to select auxiliary RAM.
* On: cause PAGE2 to switch main RAM areas.
Doesn't exist on a II/II+.
*/
void set_80_store(bool);
bool get_80_store();
/*!
Setter for PAGE2 ($C054/$C055; triggers on read or write).
* Off: select Page 1.
* On: select Page 2 or, if 80STORE on, Page 1 in auxiliary memory.
80STORE doesn't exist on a II/II+; therefore this always selects
either Page 1 or Page 2 on those machines.
*/
void set_page2(bool);
bool get_page2();
/*!
Setter for TEXT ($C050/$C051; triggers on read or write).
* Off: display graphics or, if MIXED on, mixed.
* On: display text.
*/
void set_text(bool);
bool get_text();
/*!
Setter for MIXED ($C052/$C053; triggers on read or write).
* Off: display only text or only graphics.
* On: if TEXT off, display text and graphics.
*/
void set_mixed(bool);
bool get_mixed();
/*!
Setter for HIRES ($C056/$C057; triggers on read or write).
* Off: if TEXT off, display low-resolution graphics.
* On: if TEXT off, display high-resolution or, if DHIRES on, double high-resolution graphics.
DHIRES doesn't exist on a II/II+; therefore this always selects
either high- or low-resolution graphics on those machines.
Despite Apple's documentation, the IIe also supports double low-resolution
graphics, which are the 80-column analogue to ordinary low-resolution 40-column
low-resolution graphics.
*/
void set_high_resolution(bool);
bool get_high_resolution();
/*!
Setter for annunciator 3.
* On: turn on annunciator 3.
* Off: turn off annunciator 3.
This exists on both the II/II+ and the IIe, but has no effect on
video on the older machines. It's intended to be used on the IIe
to confirm double-high resolution mode but has side effects in
selecting mixed mode output and discarding high-resolution
delay bits.
*/
void set_annunciator_3(bool);
bool get_annunciator_3();
// Setup for text mode.
void set_character_rom(const std::vector<uint8_t> &);
protected:
Outputs::CRT::CRT crt_;
@ -162,45 +58,13 @@ class VideoBase {
uint8_t *pixel_pointer_ = nullptr;
// State affecting logical state.
int row_ = 0, column_ = 0, flash_ = 0;
uint8_t flash_mask() {
return uint8_t((flash_ / flash_length) * 0xff);
}
// Enumerates all Apple II and IIe display modes.
enum class GraphicsMode {
Text = 0,
DoubleText,
HighRes,
DoubleHighRes,
LowRes,
DoubleLowRes,
FatLowRes
};
bool is_text_mode(GraphicsMode m) { return m <= GraphicsMode::DoubleText; }
bool is_double_mode(GraphicsMode m) { return !!(int(m)&1); }
// Various soft-switch values.
bool alternative_character_set_ = false, set_alternative_character_set_ = false;
bool columns_80_ = false, set_columns_80_ = false;
bool store_80_ = false, set_store_80_ = false;
bool page2_ = false, set_page2_ = false;
bool text_ = true, set_text_ = true;
bool mixed_ = false, set_mixed_ = false;
bool high_resolution_ = false, set_high_resolution_ = false;
bool annunciator_3_ = false, set_annunciator_3_ = false;
int row_ = 0, column_ = 0;
// Graphics carry is the final level output in a fetch window;
// it carries on into the next if it's high resolution with
// the delay bit set.
mutable uint8_t graphics_carry_ = 0;
bool was_double_ = false;
uint8_t high_resolution_mask_ = 0xff;
// This holds a copy of the character ROM. The regular character
// set is assumed to be in the first 64*8 bytes; the alternative
// is in the 128*8 bytes after that.
std::vector<uint8_t> character_rom_;
// Memory is fetched ahead of time into this array;
// this permits the correct delay between fetching
@ -208,16 +72,7 @@ class VideoBase {
std::array<uint8_t, 40> base_stream_;
std::array<uint8_t, 40> auxiliary_stream_;
bool is_iie_ = false;
static constexpr int flash_length = 8406;
// Describes the current text mode mapping from in-memory character index
// to output character.
struct CharacterMapping {
uint8_t address_mask;
uint8_t xor_mask;
};
CharacterMapping character_zones[4];
const bool is_iie_ = false;
/*!
Outputs 40-column text to @c target, using @c length bytes from @c source.
@ -256,9 +111,6 @@ class VideoBase {
clock rather than the 14M.
*/
void output_fat_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const;
// Maintain a DeferredQueue for delayed mode switches.
DeferredQueuePerformer<Cycles> deferrer_;
};
template <class BusHandler, bool is_iie> class Video: public VideoBase {
@ -268,13 +120,6 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
VideoBase(is_iie, [this] (Cycles cycles) { advance(cycles); }),
bus_handler_(bus_handler) {}
/*!
Runs video for @c cycles.
*/
void run_for(Cycles cycles) {
deferrer_.run_for(cycles);
}
/*!
Obtains the last value the video read prior to time now+offset.
*/
@ -329,7 +174,10 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
// Apply carry into the row counter and test it for location.
int mapped_row = row_ + (mapped_column / 65);
return (mapped_row % 262) >= 192;
// Per http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.040.html
// "on the IIe, the screen is blanked when the bit is low".
return (mapped_row % 262) < 192;
}
private:
@ -345,7 +193,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
A frame is oriented around 65 cycles across, 262 lines down.
*/
constexpr int first_sync_line = 220; // A complete guess. Information needed.
constexpr int first_sync_line = 220; // A complete guess. Information needed.
constexpr int first_sync_column = 49; // Also a guess.
constexpr int sync_length = 4; // One of the two likely candidates.
@ -422,7 +270,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
const int pixel_end = std::min(40, ending_column);
const int pixel_row = row_ & 7;
const bool is_double = Video::is_double_mode(line_mode);
const bool is_double = is_double_mode(line_mode);
if(!is_double && was_double_ && pixel_pointer_) {
pixel_pointer_[pixel_start*14 + 0] =
pixel_pointer_[pixel_start*14 + 1] =
@ -568,10 +416,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
column_ = (column_ + cycles_this_line) % 65;
if(!column_) {
row_ = (row_ + 1) % 262;
flash_ = (flash_ + 1) % (2 * flash_length);
if(!alternative_character_set_) {
character_zones[1].xor_mask = flash_mask();
}
did_end_line();
// Add an extra half a colour cycle of blank; this isn't counted in the run_for
// count explicitly but is promised. If this is a vertical sync line, output sync
@ -585,35 +430,6 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
}
}
GraphicsMode graphics_mode(int row) {
if(
text_ ||
(mixed_ && row >= 160 && row < 192)
) return columns_80_ ? GraphicsMode::DoubleText : GraphicsMode::Text;
if(high_resolution_) {
return (annunciator_3_ && columns_80_) ? GraphicsMode::DoubleHighRes : GraphicsMode::HighRes;
} else {
if(columns_80_) return GraphicsMode::DoubleLowRes;
if(annunciator_3_) return GraphicsMode::FatLowRes;
return GraphicsMode::LowRes;
}
}
int video_page() {
return (store_80_ || !page2_) ? 0 : 1;
}
uint16_t get_row_address(int row) {
const int character_row = row >> 3;
const int pixel_row = row & 7;
const uint16_t row_address = uint16_t((character_row >> 3) * 40 + ((character_row&7) << 7));
const GraphicsMode pixel_mode = graphics_mode(row);
return ((pixel_mode == GraphicsMode::HighRes) || (pixel_mode == GraphicsMode::DoubleHighRes)) ?
uint16_t(((video_page()+1) * 0x2000) + row_address + ((pixel_row&7) << 10)) :
uint16_t(((video_page()+1) * 0x400) + row_address);
}
BusHandler &bus_handler_;
};
@ -621,4 +437,4 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
}
}
#endif /* Video_hpp */
#endif /* Apple_II_Video_hpp */

View File

@ -0,0 +1,377 @@
//
// VideoSwitches.hpp
// Clock Signal
//
// Created by Thomas Harte on 31/10/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef VideoSwitches_h
#define VideoSwitches_h
#include "../../../ClockReceiver/ClockReceiver.hpp"
#include "../../../ClockReceiver/DeferredQueue.hpp"
#include "../../ROMMachine.hpp"
namespace Apple {
namespace II {
// Enumerates all Apple II and IIe display modes.
enum class GraphicsMode {
Text = 0,
DoubleText,
HighRes,
DoubleHighRes,
LowRes,
DoubleLowRes,
/// Fat low res mode is regular low res mode, but clocked out at 7Mhz rather than 14, leading to improper colours.
FatLowRes
};
constexpr bool is_text_mode(GraphicsMode m) { return m <= GraphicsMode::DoubleText; }
constexpr bool is_double_mode(GraphicsMode m) { return int(m) & 1; }
template <typename TimeUnit> class VideoSwitches {
public:
/*!
Constructs a new instance of VideoSwitches in which changes to relevant switches
affect the video mode only after @c delay cycles.
If @c is_iie is true, these switches will set up the character zones for an IIe-esque
set of potential flashing characters and alternate video modes.
*/
VideoSwitches(bool is_iie, TimeUnit delay, std::function<void(TimeUnit)> &&target) : delay_(delay), deferrer_(std::move(target)) {
character_zones_[0].xor_mask = 0;
character_zones_[0].address_mask = 0x3f;
character_zones_[1].xor_mask = 0;
character_zones_[1].address_mask = 0x3f;
character_zones_[2].xor_mask = 0;
character_zones_[2].address_mask = 0x3f;
character_zones_[3].xor_mask = 0;
character_zones_[3].address_mask = 0x3f;
if(is_iie) {
character_zones_[0].xor_mask =
character_zones_[2].xor_mask =
character_zones_[3].xor_mask = 0xff;
character_zones_[2].address_mask =
character_zones_[3].address_mask = 0xff;
}
}
/*!
Advances @c cycles.
*/
void run_for(TimeUnit cycles) {
deferrer_.run_for(cycles);
}
/*
Descriptions for the setters below are taken verbatim from
the Apple IIe Technical Reference. Addresses are the conventional
locations within the Apple II memory map. Only those which affect
video output are implemented here.
Those registers which don't exist on a II/II+ are marked.
*/
/*!
Setter for ALTCHAR ($C00E/$C00F; triggers on write only):
* Off: display text using primary character set.
* On: display text using alternate character set.
Doesn't exist on a II/II+.
*/
void set_alternative_character_set(bool alternative_character_set) {
external_.alternative_character_set = alternative_character_set;
deferrer_.defer(delay_, [this, alternative_character_set] {
internal_.alternative_character_set = alternative_character_set;
if(alternative_character_set) {
character_zones_[1].address_mask = 0xff;
character_zones_[1].xor_mask = 0;
} else {
character_zones_[1].address_mask = 0x3f;
character_zones_[1].xor_mask = flash_mask();
// The XOR mask is seeded here; it's dynamic, so updated elsewhere.
}
});
}
bool get_alternative_character_set() const {
return external_.alternative_character_set;
}
/*!
Setter for 80COL ($C00C/$C00D; triggers on write only).
* Off: display 40 columns.
* On: display 80 columns.
Doesn't exist on a II/II+.
*/
void set_80_columns(bool columns_80) {
external_.columns_80 = columns_80;
deferrer_.defer(delay_, [this, columns_80] {
internal_.columns_80 = columns_80;
});
}
bool get_80_columns() const {
return external_.columns_80;
}
/*!
Setter for 80STORE ($C000/$C001; triggers on write only).
* Off: cause PAGE2 to select auxiliary RAM.
* On: cause PAGE2 to switch main RAM areas.
Doesn't exist on a II/II+.
*/
void set_80_store(bool store_80) {
external_.store_80 = internal_.store_80 = store_80;
}
bool get_80_store() const {
return external_.store_80;
}
/*!
Setter for PAGE2 ($C054/$C055; triggers on read or write).
* Off: select Page 1.
* On: select Page 2 or, if 80STORE on, Page 1 in auxiliary memory.
80STORE doesn't exist on a II/II+; therefore this always selects
either Page 1 or Page 2 on those machines.
*/
void set_page2(bool page2) {
external_.page2 = internal_.page2 = page2;
}
bool get_page2() const {
return external_.page2;
}
/*!
Setter for TEXT ($C050/$C051; triggers on read or write).
* Off: display graphics or, if MIXED on, mixed.
* On: display text.
*/
void set_text(bool text) {
external_.text = text;
deferrer_.defer(delay_, [this, text] {
internal_.text = text;
});
}
bool get_text() const {
return external_.text;
}
/*!
Setter for MIXED ($C052/$C053; triggers on read or write).
* Off: display only text or only graphics.
* On: if TEXT off, display text and graphics.
*/
void set_mixed(bool mixed) {
external_.mixed = mixed;
deferrer_.defer(delay_, [this, mixed] {
internal_.mixed = mixed;
});
}
bool get_mixed() const {
return external_.mixed;
}
/*!
Setter for HIRES ($C056/$C057; triggers on read or write).
* Off: if TEXT off, display low-resolution graphics.
* On: if TEXT off, display high-resolution or, if DHIRES on, double high-resolution graphics.
DHIRES doesn't exist on a II/II+; therefore this always selects
either high- or low-resolution graphics on those machines.
Despite Apple's documentation, the IIe also supports double low-resolution
graphics, which are the 80-column analogue to ordinary low-resolution 40-column
low-resolution graphics.
*/
void set_high_resolution(bool high_resolution) {
external_.high_resolution = high_resolution;
deferrer_.defer(delay_, [this, high_resolution] {
internal_.high_resolution = high_resolution;
});
}
bool get_high_resolution() const {
return external_.high_resolution;
}
/*!
Setter for annunciator 3.
* On: turn on annunciator 3.
* Off: turn off annunciator 3.
This exists on both the II/II+ and the IIe, but has no effect on
video on the older machines. It's intended to be used on the IIe
to confirm double-high resolution mode but has side effects in
selecting mixed mode output and discarding high-resolution
delay bits.
*/
void set_annunciator_3(bool annunciator_3) {
external_.annunciator_3 = annunciator_3;
deferrer_.defer(delay_, [this, annunciator_3] {
internal_.annunciator_3 = annunciator_3;
high_resolution_mask_ = annunciator_3 ? 0x7f : 0xff;
});
}
bool get_annunciator_3() const {
return external_.annunciator_3;
}
enum class CharacterROM {
/// The ROM that shipped with both the Apple II and the II+.
II,
/// The ROM that shipped with the original IIe.
IIe,
/// The ROM that shipped with the Enhanced IIe.
EnhancedIIe,
/// The ROM that shipped with the IIgs.
IIgs
};
/// @returns A file-level description of @c rom.
static ROMMachine::ROM rom_description(CharacterROM rom) {
const std::string machine_name = "AppleII";
switch(rom) {
case CharacterROM::II:
return ROMMachine::ROM(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
case CharacterROM::IIe:
return ROMMachine::ROM(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1);
default: // To appease GCC.
case CharacterROM::EnhancedIIe:
return ROMMachine::ROM(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d);
case CharacterROM::IIgs:
return ROMMachine::ROM(machine_name, "the Apple IIgs character ROM", "apple2gs.chr", 4*1024, 0x91e53cd8);
}
}
/// Set the character ROM for this video output.
void set_character_rom(const std::vector<uint8_t> &rom) {
character_rom_ = rom;
// There's some inconsistency in bit ordering amongst the common ROM dumps;
// detect that based arbitrarily on the second line of the $ graphic and
// ensure consistency.
if(character_rom_[0x121] == 0x3c || character_rom_[0x122] == 0x3c) {
for(auto &graphic : character_rom_) {
graphic =
((graphic & 0x01) ? 0x40 : 0x00) |
((graphic & 0x02) ? 0x20 : 0x00) |
((graphic & 0x04) ? 0x10 : 0x00) |
((graphic & 0x08) ? 0x08 : 0x00) |
((graphic & 0x10) ? 0x04 : 0x00) |
((graphic & 0x20) ? 0x02 : 0x00) |
((graphic & 0x40) ? 0x01 : 0x00);
}
}
}
protected:
GraphicsMode graphics_mode(int row) const {
if(
internal_.text ||
(internal_.mixed && row >= 160 && row < 192)
) return internal_.columns_80 ? GraphicsMode::DoubleText : GraphicsMode::Text;
if(internal_.high_resolution) {
return (internal_.annunciator_3 && internal_.columns_80) ? GraphicsMode::DoubleHighRes : GraphicsMode::HighRes;
} else {
if(internal_.columns_80) return GraphicsMode::DoubleLowRes;
if(internal_.annunciator_3) return GraphicsMode::FatLowRes;
return GraphicsMode::LowRes;
}
}
int video_page() const {
return (internal_.store_80 || !internal_.page2) ? 0 : 1;
}
uint16_t get_row_address(int row) const {
const int character_row = row >> 3;
const int pixel_row = row & 7;
const uint16_t row_address = uint16_t((character_row >> 3) * 40 + ((character_row&7) << 7));
const GraphicsMode pixel_mode = graphics_mode(row);
return ((pixel_mode == GraphicsMode::HighRes) || (pixel_mode == GraphicsMode::DoubleHighRes)) ?
uint16_t(((video_page()+1) * 0x2000) + row_address + ((pixel_row&7) << 10)) :
uint16_t(((video_page()+1) * 0x400) + row_address);
}
/*!
Should be called by subclasses at the end of each line of the display;
this gives the base class a peg on which to hang flashing-character updates.
*/
void did_end_line() {
// Update character set flashing; flashing is applied only when the alternative
// character set is not selected.
flash_ = (flash_ + 1) % (2 * flash_length);
character_zones_[1].xor_mask = flash_mask() * !internal_.alternative_character_set;
}
private:
// Maintain a DeferredQueue for delayed mode switches.
const TimeUnit delay_;
DeferredQueuePerformer<TimeUnit> deferrer_;
struct Switches {
bool alternative_character_set = false;
bool columns_80 = false;
bool store_80 = false;
bool page2 = false;
bool text = true;
bool mixed = false;
bool high_resolution = false;
bool annunciator_3 = false;
} external_, internal_;
int flash_length = 8406;
int flash_ = 0;
uint8_t flash_mask() const {
return uint8_t((flash_ / flash_length) * 0xff);
}
protected:
// Describes the current text mode mapping from in-memory character index
// to output character; subclasses should:
//
// (i) use the top two-bits of the character code to index character_zones_;
// (ii) apply the address_mask to the character code in order to get a character
// offset into the character ROM; and
// (iii) apply the XOR mask to the output of the character ROM.
//
// By this means they will properly handle the limited character sets of Apple IIs
// prior to the IIe as well as the IIe and onward's alternative character set toggle.
struct CharacterMapping {
uint8_t address_mask;
uint8_t xor_mask;
};
CharacterMapping character_zones_[4];
// A mask that should be applied to high-resolution graphics bytes before output;
// it acts to retain or remove the top bit, affecting whether the half-pixel delay
// bit is effective. On a IIe it's toggleable, on early Apple IIs it doesn't exist.
uint8_t high_resolution_mask_ = 0xff;
// This holds a copy of the character ROM. The regular character
// set is assumed to be in the first 64*8 bytes; the alternative
// is in the 128*8 bytes after that.
std::vector<uint8_t> character_rom_;
};
}
}
#endif /* VideoSwitches_h */

View File

@ -0,0 +1,290 @@
//
// ADB.cpp
// Clock Signal
//
// Created by Thomas Harte on 31/10/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#include "ADB.hpp"
#include <cassert>
#include <cstdio>
#include <iostream>
// TEST.
#include "../../../InstructionSets/M50740/Parser.hpp"
#include "../../../InstructionSets/Disassembler.hpp"
#define LOG_PREFIX "[ADB GLU] "
#include "../../../Outputs/Log.hpp"
using namespace Apple::IIgs::ADB;
namespace {
// Flags affecting the CPU-visible status register.
enum class CPUFlags: uint8_t {
MouseDataFull = 0x80,
MouseInterruptEnabled = 0x40,
CommandDataIsValid = 0x20,
CommandDataInterruptEnabled = 0x10,
KeyboardDataFull = 0x08,
KeyboardDataInterruptEnabled = 0x04,
MouseXIsAvailable = 0x02,
CommandRegisterFull = 0x01,
};
// Flags affecting the microcontroller-visible register.
enum class MicrocontrollerFlags: uint8_t {
CommandRegisterFull = 0x40,
};
}
GLU::GLU() :
executor_(*this),
bus_(HalfCycles(1'789'772)),
controller_id_(bus_.add_device()),
mouse_(bus_),
keyboard_(bus_) {}
// MARK: - External interface.
uint8_t GLU::get_keyboard_data() {
// The classic Apple II serial keyboard register:
// b7: key strobe.
// b6b0: ASCII code.
return (registers_[0] & 0x7f) | ((status_ & uint8_t(CPUFlags::KeyboardDataFull)) ? 0x80 : 0x00);
}
void GLU::clear_key_strobe() {
// Clears the key strobe of the classic Apple II serial keyboard register.
status_ &= ~uint8_t(CPUFlags::KeyboardDataFull);
}
uint8_t GLU::get_any_key_down() {
// The Apple IIe check-for-any-key-down bit.
return registers_[5];
}
uint8_t GLU::get_mouse_data() {
// Alternates between returning x and y values.
//
// b7: 1 = button is up; 0 = button is down.
// b6: delta sign bit; 1 = negative.
// b5b0: mouse delta.
const uint8_t result = registers_[visible_mouse_register_];
if(visible_mouse_register_ == 2) {
++visible_mouse_register_;
} else {
status_ &= ~uint8_t(CPUFlags::MouseDataFull);
}
return result;
}
uint8_t GLU::get_modifier_status() {
// b7: 1 = command key pressed; 0 = not.
// b6: option key.
// b5: 1 = modifier key latch has been updated, no key has been pressed; 0 = not.
// b4: any numeric keypad key.
// b3: a key is down.
// b2: caps lock is pressed.
// b1: control key.
// b0: shift key.
return registers_[6];
}
uint8_t GLU::get_data() {
// b02: number of data bytes to be returned.
// b3: 1 = a valid service request is pending; 0 = no request pending.
// b4: 1 = control, command and delete keys have been pressed simultaneously; 0 = they haven't.
// b5: 1 = control, command and reset have all been pressed together; 0 = they haven't.
// b6: 1 = ADB controller encountered an error and reset itself; 0 = no error.
// b7: 1 = ADB has received a response from the addressed ADB device; 0 = no respone.
status_ &= ~uint8_t(CPUFlags::CommandDataIsValid);
return registers_[7];
}
uint8_t GLU::get_status() {
// b7: 1 = mouse data register is full; 0 = empty.
// b6: 1 = mouse interrupt is enabled.
// b5: 1 = command/data has valid data.
// b4: 1 = command/data interrupt is enabled.
// b3: 1 = keyboard data is full.
// b2: 1 = keyboard data interrupt is enabled.
// b1: 1 = mouse x-data is available; 0 = y.
// b0: 1 = command register is full (set when command is written); 0 = empty (cleared when data is read).
return status_ | ((visible_mouse_register_ == 2) ? uint8_t(CPUFlags::MouseXIsAvailable) : 0);
}
void GLU::set_status(uint8_t status) {
// This permits only the interrupt flags to be set.
constexpr uint8_t interrupt_flags =
uint8_t(CPUFlags::MouseInterruptEnabled) |
uint8_t(CPUFlags::CommandDataInterruptEnabled) |
uint8_t(CPUFlags::KeyboardDataInterruptEnabled);
status_ = (status_ & ~interrupt_flags) | (status & interrupt_flags);
}
void GLU::set_command(uint8_t command) {
registers_[1] = command;
registers_[4] |= uint8_t(MicrocontrollerFlags::CommandRegisterFull);
status_ |= uint8_t(CPUFlags::CommandRegisterFull);
}
// MARK: - Setup and run.
void GLU::set_microcontroller_rom(const std::vector<uint8_t> &rom) {
executor_.set_rom(rom);
// TEST invocation.
/* InstructionSet::Disassembler<InstructionSet::M50740::Parser, 0x1fff, InstructionSet::M50740::Instruction, uint8_t, uint16_t> disassembler;
disassembler.disassemble(rom.data(), 0x1000, uint16_t(rom.size()), 0x1000);
const auto instructions = disassembler.instructions();
const auto entry_points = disassembler.entry_points();
for(const auto &pair : instructions) {
std::cout << std::hex << pair.first << "\t\t";
if(entry_points.find(pair.first) != entry_points.end()) {
std::cout << "L" << pair.first << "\t";
} else {
std::cout << "\t\t";
}
std::cout << operation_name(pair.second.operation) << " ";
std::cout << address(pair.second.addressing_mode, &rom[pair.first - 0x1000], pair.first);
std::cout << std::endl;
}*/
}
void GLU::run_for(Cycles cycles) {
executor_.run_for(cycles);
}
// MARK: - M50470 port handler
void GLU::set_port_output(int port, uint8_t value) {
switch(port) {
case 0:
register_latch_ = value;
break;
case 1:
// printf("Keyboard write: %02x???\n", value);
break;
case 2: {
// printf("ADB data line input: %d???\n", value >> 7);
// printf("IIe keyboard reset line: %d\n", (value >> 6)&1);
// printf("IIgs reset line: %d\n", (value >> 5)&1);
// printf("GLU strobe: %d\n", (value >> 4)&1);
// printf("Select GLU register: %d [%02x]\n", value & 0xf, value);
register_address_ = value & 0xf;
// This is an ugly hack, I think. Per Neil Parker's Inside the Apple IIGS ADB Controller
// http://nparker.llx.com/a2/adb.html#external:
//
// The protocol for reading an ADB GLU register is as follows:
//
// 1. Put the register number of the ADB GLU register in port P2 bits 0-3.
// 2. Clear bit 4 of port P2, read the data from P0, and set bit 4 of P0.
//
// The protocol for writing a GLU register is similar:
//
// 1. Write the register number to port P2 bits 0-3.
// 2. Write the data to port P0.
// 3. Configure port P0 for output by writing $FF to $E1.
// 4. Clear bit 4 of P2, and immediately set it again.
// 5. Configure port P0 for input by writing 0 to $E1.
//
// ---
//
// I tried: linking a read or write to rising or falling edges of the strobe.
// Including with hysteresis as per the "immediately" (which, in practice, seems
// to mean "in the very next instruction", i.e. 5 cycles later). That didn't seem
// properly to differentiate.
//
// So I'm focussing on the "configure port P0 for output" bit. Which I don't see
// would be visible here unless it is actually an exposed signal, which is unlikely.
//
// Ergo: ugly. HACK.
const bool strobe = value & 0x10;
if(strobe != register_strobe_) {
register_strobe_ = strobe;
if(!register_strobe_) {
if(executor_.get_output_mask(0)) {
registers_[register_address_] = register_latch_;
switch(register_address_) {
default: break;
case 0: status_ |= uint8_t(CPUFlags::KeyboardDataFull); break;
case 2:
case 3:
status_ |= uint8_t(CPUFlags::MouseDataFull);
visible_mouse_register_ = 2;
printf("Mouse: %d <- %02x\n", register_address_, register_latch_);
break;
case 7: status_ |= uint8_t(CPUFlags::CommandDataIsValid); break;
}
} else {
register_latch_ = registers_[register_address_];
switch(register_address_) {
default: break;
case 1:
registers_[4] &= ~uint8_t(MicrocontrollerFlags::CommandRegisterFull);
status_ &= ~uint8_t(CPUFlags::CommandRegisterFull);
break;
}
}
}
}
} break;
case 3:
if(modifier_state_ != (value & 0x30)) {
modifier_state_ = value & 0x30;
LOG("Modifier state: " << int(value & 0x30));
}
// Output is inverted respective to input; the microcontroller
// sets a value of '1' in order to pull the ADB bus low.
bus_.set_device_output(controller_id_, !(value & 0x08));
break;
default: assert(false);
}
}
bool GLU::get_command_button() const {
return modifier_state_ & 0x20;
}
bool GLU::get_option_button() const {
return modifier_state_ & 0x10;
}
uint8_t GLU::get_port_input(int port) {
switch(port) {
case 0: return register_latch_;
case 1:
// printf("IIe keyboard read\n");
return 0x06;
case 2:
// printf("ADB data line input, etc\n");
return bus_.get_state() ? 0x80 : 0x00;
case 3:
// printf("ADB data line output, etc\n");
return 0x00;
default: assert(false);
}
return 0xff;
}
void GLU::run_ports_for(Cycles cycles) {
bus_.run_for(cycles);
}
void GLU::set_vertical_blank(bool is_blank) {
executor_.set_interrupt_line(is_blank);
}

View File

@ -0,0 +1,92 @@
//
// ADB.hpp
// Clock Signal
//
// Created by Thomas Harte on 31/10/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef Apple_IIgs_ADB_hpp
#define Apple_IIgs_ADB_hpp
#include <cstdint>
#include <vector>
#include "../../../InstructionSets/M50740/Executor.hpp"
#include "../ADB/Bus.hpp"
#include "../ADB/Mouse.hpp"
#include "../ADB/Keyboard.hpp"
namespace Apple {
namespace IIgs {
namespace ADB {
class GLU: public InstructionSet::M50740::PortHandler {
public:
GLU();
uint8_t get_keyboard_data();
uint8_t get_mouse_data();
uint8_t get_modifier_status();
uint8_t get_any_key_down();
uint8_t get_data();
uint8_t get_status();
void set_command(uint8_t);
void set_status(uint8_t);
void clear_key_strobe();
void set_microcontroller_rom(const std::vector<uint8_t> &rom);
void run_for(Cycles cycles);
bool get_command_button() const;
bool get_option_button() const;
void set_vertical_blank(bool);
bool get_vertical_blank() {
return vertical_blank_;
}
Apple::ADB::Keyboard &keyboard() {
return keyboard_;
}
Inputs::Mouse &get_mouse() {
return mouse_;
}
private:
InstructionSet::M50740::Executor executor_;
void run_ports_for(Cycles) override;
void set_port_output(int port, uint8_t value) override;
uint8_t get_port_input(int port) override;
uint8_t registers_[16]{};
uint8_t register_address_;
uint8_t register_latch_ = 0xff;
bool register_strobe_ = false;
uint8_t status_ = 0x00;
Apple::ADB::Bus bus_;
size_t controller_id_;
uint8_t modifier_state_ = 0;
bool vertical_blank_ = false;
int visible_mouse_register_ = 2;
// For now, attach only a keyboard and mouse.
Apple::ADB::Mouse mouse_;
Apple::ADB::Keyboard keyboard_;
};
}
}
}
#endif /* Apple_IIgs_ADB_hpp */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
//
// AppleIIgs.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/10/2020.
// Copyright 2020 Thomas Harte. All rights reserved.
//
#ifndef Machines_Apple_AppleIIgs_hpp
#define Machines_Apple_AppleIIgs_hpp
#include "../../../Configurable/Configurable.hpp"
#include "../../../Configurable/StandardOptions.hpp"
#include "../../../Analyser/Static/StaticAnalyser.hpp"
#include "../../ROMMachine.hpp"
#include <memory>
namespace Apple {
namespace IIgs {
class Machine {
public:
virtual ~Machine();
/// Creates and returns an AppleIIgs.
static Machine *AppleIIgs(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
};
}
}
#endif /* Machines_Apple_AppleIIgs_hpp */

View File

@ -0,0 +1,597 @@
//
// MemoryMap.hpp
// Clock Signal
//
// Created by Thomas Harte on 25/10/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef Machines_Apple_AppleIIgs_MemoryMap_hpp
#define Machines_Apple_AppleIIgs_MemoryMap_hpp
#include <array>
#include <bitset>
#include <vector>
#include "../AppleII/LanguageCardSwitches.hpp"
#include "../AppleII/AuxiliaryMemorySwitches.hpp"
namespace Apple {
namespace IIgs {
class MemoryMap {
public:
// MARK: - Initial construction and configuration.
MemoryMap() : auxiliary_switches_(*this), language_card_(*this) {}
void set_storage(std::vector<uint8_t> &ram, std::vector<uint8_t> &rom) {
// Keep a pointer for later; also note the proper RAM offset.
ram_base = ram.data();
shadow_base[0] = ram_base; // i.e. all unshadowed writes go to where they've already gone (to make a no-op).
shadow_base[1] = &ram[ram.size() - 0x02'0000]; // i.e. all shadowed writes go somewhere in the last
// 128bk of RAM.
// Establish bank mapping.
uint8_t next_region = 0;
auto region = [&next_region, this]() -> uint8_t {
assert(next_region != regions.size());
return next_region++;
};
auto set_region = [this](uint8_t bank, uint16_t start, uint16_t end, uint8_t region) {
assert((end == 0xffff) || !(end&0xff));
assert(!(start&0xff));
// Fill in memory map.
size_t target = size_t((bank << 8) | (start >> 8));
for(int c = start; c < end; c += 0x100) {
region_map[target] = region;
++target;
}
};
auto set_regions = [this, set_region, region](uint8_t bank, std::initializer_list<uint16_t> addresses, std::vector<uint8_t> allocated_regions = {}) {
uint16_t previous = 0x0000;
auto next_region = allocated_regions.begin();
for(uint16_t address: addresses) {
set_region(bank, previous, address, next_region != allocated_regions.end() ? *next_region : region());
previous = address;
assert(next_region != allocated_regions.end() || allocated_regions.empty());
if(next_region != allocated_regions.end()) ++next_region;
}
assert(next_region == allocated_regions.end());
};
// Current beliefs about the IIgs memory map:
//
// * language card banking applies to banks $00, $01, $e0 and $e1;
// * auxiliary memory switches apply to bank $00 only;
// * shadowing may be enabled only on banks $00 and $01, or on all RAM pages; and
// * whether bit 16 of the address is passed to the Mega II is selectable — this affects both the destination
// of odd-bank shadows, and whether bank $e1 is actually distinct from $e0.
//
// So:
//
// * bank $00 needs to be divided by auxiliary and language card zones;
// * banks $01, $e0 and $e1 need to be divided by language card zones only; and
// * ROM banks and all other fast RAM banks don't need subdivision.
// Language card zones:
//
// $D000$E000 4kb window, into either bank 1 or bank 2
// $E000end 12kb window, always the same RAM.
// Auxiliary zones:
//
// $0000$0200 Zero page (and stack)
// $0200$0400 [space in between]
// $0400$0800 Text Page 1
// $0800$2000 [space in between]
// $2000$4000 High-res Page 1
// $4000$C000 [space in between]
// Card zones:
//
// $C100$C2FF either cards or IIe-style ROM
// $C300$C3FF IIe-supplied 80-column card replacement ROM
// $C400$C7FF either cards or IIe-style ROM
// $C800$CFFF Standard extended card area
// Reserve region 0 as that for unmapped memory.
region();
// Bank $00: all locations potentially affected by the auxiliary switches or the
// language switches.
set_regions(0x00, {
0x0200, 0x0400, 0x0800,
0x2000, 0x4000,
0xc000, 0xc100, 0xc300, 0xc400, 0xc800,
0xd000, 0xe000,
0xffff
});
// Bank $01: all locations potentially affected by the language switches and card switches.
set_regions(0x01, {
0xc000, 0xc100, 0xc300, 0xc400, 0xc800,
0xd000, 0xe000,
0xffff
});
// Banks $02[end of RAM]: a single region.
const auto fast_region = region();
const uint8_t fast_ram_bank_limit = uint8_t(ram.size() / 0x01'0000);
for(uint8_t bank = 0x02; bank < fast_ram_bank_limit; bank++) {
set_region(bank, 0x0000, 0xffff, fast_region);
}
// [Banks $80$e0: empty].
// Banks $e0, $e1: all locations potentially affected by the language switches or marked for IO.
// Alas, separate regions are needed due to the same ROM appearing on both pages.
for(uint8_t c = 0; c < 2; c++) {
set_regions(0xe0 + c, {0xc000, 0xc100, 0xc300, 0xc400, 0xc800, 0xd000, 0xe000, 0xffff});
}
// [Banks $e2[ROM start]: empty].
// ROM banks: directly mapped to ROM.
const uint8_t rom_bank_count = uint8_t(rom.size() >> 16);
const uint8_t first_rom_bank = uint8_t(0x100 - rom_bank_count);
const uint8_t rom_region = region();
for(uint8_t c = 0; c < rom_bank_count; ++c) {
set_region(first_rom_bank + c, 0x0000, 0xffff, rom_region);
}
// Apply proper storage to those banks.
auto set_storage = [this](uint32_t address, const uint8_t *read, uint8_t *write) {
// Don't allow the reserved null region to be modified.
assert(region_map[address >> 8]);
// Either set or apply a quick bit of testing as to the logic at play.
auto &region = regions[region_map[address >> 8]];
if(read) read -= address;
if(write) write -= address;
if(!region.read) {
region.read = read;
region.write = write;
} else {
assert(region.read == read);
assert(region.write == write);
}
};
// This is highly redundant, but decouples this step from the above.
for(size_t c = 0; c < 0x80'0000; c += 0x100) {
if(c < ram.size() - 0x02'0000) {
set_storage(uint32_t(c), &ram[c], &ram[c]);
}
}
uint8_t *const slow_ram = &ram[ram.size() - 0x02'0000] - 0xe0'0000;
for(size_t c = 0xe0'0000; c < 0xe2'0000; c += 0x100) {
set_storage(uint32_t(c), &slow_ram[c], &slow_ram[c]);
}
for(uint32_t c = 0; c < uint32_t(rom_bank_count); c++) {
set_storage((first_rom_bank + c) << 16, &rom[c << 16], nullptr);
}
// Set shadowing as working from banks 0 and 1 (forever).
shadow_banks[0] = true;
// TODO: set 1Mhz flags.
// Apply initial language/auxiliary state.
set_all_paging();
}
// MARK: - Live bus access notifications and register access.
void set_shadow_register(uint8_t value) {
const uint8_t diff = value ^ shadow_register_;
shadow_register_ = value;
if(diff & 0x40) { // IO/language-card inhibit.
set_language_card_paging();
set_card_paging();
}
if(diff & 0x3f) {
set_shadowing();
}
}
uint8_t get_shadow_register() const {
return shadow_register_;
}
void set_speed_register(uint8_t value) {
speed_register_ = value;
// Enable or disable shadowing from banks 0x020x80.
for(size_t c = 0x01; c < 0x40; c++) {
shadow_banks[c] = speed_register_ & 0x10;
}
}
void set_state_register(uint8_t value) {
auxiliary_switches_.set_state(value);
language_card_.set_state(value);
}
uint8_t get_state_register() const {
return language_card_.get_state() | auxiliary_switches_.get_state();
}
void access(uint16_t address, bool is_read) {
auxiliary_switches_.access(address, is_read);
if((address & 0xfff0) == 0xc080) language_card_.access(address, is_read);
}
using AuxiliaryMemorySwitches = Apple::II::AuxiliaryMemorySwitches<MemoryMap>;
const AuxiliaryMemorySwitches &auxiliary_switches() const {
return auxiliary_switches_;
}
using LanguageCardSwitches = Apple::II::LanguageCardSwitches<MemoryMap>;
const LanguageCardSwitches &language_card_switches() const {
return language_card_;
}
private:
AuxiliaryMemorySwitches auxiliary_switches_;
LanguageCardSwitches language_card_;
friend AuxiliaryMemorySwitches;
friend LanguageCardSwitches;
uint8_t shadow_register_ = 0x08;
uint8_t speed_register_ = 0x00;
// MARK: - Memory banking.
#define assert_is_region(start, end) \
assert(region_map[start] == region_map[start-1]+1); \
assert(region_map[end-1] == region_map[start]); \
assert(region_map[end] == region_map[end-1]+1);
// Cf. LanguageCardSwitches; this function should update the region from
// $D000 onwards as per the state of the language card flags — there may
// end up being ROM or RAM (or auxiliary RAM), and the first 4kb of it
// may be drawn from either of two pools.
void set_language_card_paging() {
const auto language_state = language_card_.state();
const auto zero_state = auxiliary_switches_.zero_state();
const bool inhibit_banks0001 = shadow_register_ & 0x40;
auto apply = [&language_state, this](uint32_t bank_base, uint8_t *ram) {
// This assumes bank 1 is the one before bank 2 when RAM is linear.
uint8_t *const d0_ram_bank = ram - (language_state.bank2 ? 0x0000 : 0x1000);
// Crib the ROM pointer from a page it's always visible on.
const uint8_t *const rom = &regions[region_map[0xffd0]].read[0xff'd000] - ((bank_base << 8) + 0xd000);
auto &d0_region = regions[region_map[bank_base | 0xd0]];
d0_region.read = language_state.read ? d0_ram_bank : rom;
d0_region.write = language_state.write ? nullptr : d0_ram_bank;
auto &e0_region = regions[region_map[bank_base | 0xe0]];
e0_region.read = language_state.read ? ram : rom;
e0_region.write = language_state.write ? nullptr : ram;
// Assert assumptions made above re: memory layout.
assert(region_map[bank_base | 0xd0] + 1 == region_map[bank_base | 0xe0]);
assert(region_map[bank_base | 0xe0] == region_map[bank_base | 0xff]);
};
auto set_no_card = [this](uint32_t bank_base) {
auto &d0_region = regions[region_map[bank_base | 0xd0]];
d0_region.read = ram_base;
d0_region.write = ram_base;
auto &e0_region = regions[region_map[bank_base | 0xe0]];
e0_region.read = ram_base;
e0_region.write = ram_base;
// Assert assumptions made above re: memory layout.
assert(region_map[bank_base | 0xd0] + 1 == region_map[bank_base | 0xe0]);
assert(region_map[bank_base | 0xe0] == region_map[bank_base | 0xff]);
};
if(inhibit_banks0001) {
set_no_card(0x0000);
set_no_card(0x0100);
} else {
apply(0x0000, zero_state ? &ram_base[0x01'0000] : ram_base);
apply(0x0100, ram_base);
}
// The pointer stored in region_map[0xe000] has already been adjusted for
// the 0xe0'0000 addressing offset.
uint8_t *const e0_ram = regions[region_map[0xe000]].write;
apply(0xe000, e0_ram);
apply(0xe100, e0_ram);
}
// Cf. AuxiliarySwitches; this should establish whether ROM or card switches
// are exposed in the distinct regions C100C2FF, C300C3FF, C400C7FF and
// C800CFFF.
//
// On the IIgs it intersects with the current shadow register.
//
// TODO: so... shouldn't the card mask be incorporated here? I've got it implemented
// distinctly at present, but does that create any invalid state interactions?
void set_card_paging() {
const bool inhibit_banks0001 = shadow_register_ & 0x40;
const auto state = auxiliary_switches_.card_state();
auto apply = [&state, this](uint32_t bank_base) {
auto &c0_region = regions[region_map[bank_base | 0xc0]];
auto &c1_region = regions[region_map[bank_base | 0xc1]];
auto &c3_region = regions[region_map[bank_base | 0xc3]];
auto &c4_region = regions[region_map[bank_base | 0xc4]];
auto &c8_region = regions[region_map[bank_base | 0xc8]];
const uint8_t *const rom = &regions[region_map[0xffd0]].read[0xffc100] - ((bank_base << 8) + 0xc100);
// This is applied dynamically as it may be added or lost in banks $00 and $01.
c0_region.flags |= Region::IsIO;
#define apply_region(flag, region) \
if(flag) { \
region.read = rom; \
region.flags &= ~Region::IsIO; \
} else { \
region.flags |= Region::IsIO; \
}
apply_region(state.region_C1_C3, c1_region);
apply_region(state.region_C3, c3_region);
apply_region(state.region_C4_C8, c4_region);
apply_region(state.region_C8_D0, c8_region);
#undef apply_region
// Sanity checks.
assert(region_map[bank_base | 0xc1] == region_map[bank_base | 0xc0]+1);
assert(region_map[bank_base | 0xc2] == region_map[bank_base | 0xc1]);
assert(region_map[bank_base | 0xc3] == region_map[bank_base | 0xc2]+1);
assert(region_map[bank_base | 0xc4] == region_map[bank_base | 0xc3]+1);
assert(region_map[bank_base | 0xc7] == region_map[bank_base | 0xc4]);
assert(region_map[bank_base | 0xc8] == region_map[bank_base | 0xc7]+1);
assert(region_map[bank_base | 0xcf] == region_map[bank_base | 0xc8]);
assert(region_map[bank_base | 0xd0] == region_map[bank_base | 0xcf]+1);
};
if(inhibit_banks0001) {
// Set no IO in the Cx00 range for banks $00 and $01, just
// regular RAM (or possibly auxiliary).
const auto auxiliary_state = auxiliary_switches_.main_state();
for(uint8_t region = region_map[0x00c0]; region < region_map[0x00d0]; region++) {
regions[region].read = auxiliary_state.base.read ? &ram_base[0x01'0000] : ram_base;
regions[region].write = auxiliary_state.base.write ? &ram_base[0x01'0000] : ram_base;
regions[region].flags &= ~Region::IsIO;
}
for(uint8_t region = region_map[0x01c0]; region < region_map[0x01d0]; region++) {
regions[region].read = regions[region].write = ram_base;
regions[region].flags &= ~Region::IsIO;
}
} else {
// Obey the card state for banks $00 and $01.
apply(0x0000);
apply(0x0100);
}
// Obey the card state for banks $e0 and $e1.
apply(0xe000);
apply(0xe100);
}
// Cf. LanguageCardSwitches; this should update whether base or auxiliary RAM is
// visible in: (i) the zero and stack pages; and (ii) anywhere that the language
// card is exposing RAM instead of ROM.
void set_zero_page_paging() {
// Affects bank $00 only, and should be a single region.
auto &region = regions[region_map[0]];
region.read = region.write = auxiliary_switches_.zero_state() ? &ram_base[0x01'0000] : ram_base;
assert(region_map[0x0000] == region_map[0x0001]);
assert(region_map[0x0001]+1 == region_map[0x0002]);
// Switching to or from auxiliary RAM potentially affects the
// language card area.
set_language_card_paging();
}
// IIgs specific: sets or resets the ::IsShadowed flag across affected banks as
// per the current state of the shadow register.
//
// Completely distinct from the auxiliary and language card switches.
void set_shadowing() {
// Relevant bits:
//
// b5: inhibit shadowing, text page 2 [if ROM 03; as if always set otherwise]
// b4: inhibit shadowing, auxiliary high-res graphics
// b3: inhibit shadowing, super high-res graphics
// b2: inhibit shadowing, high-res graphics page 2
// b1: inhibit shadowing, high-res graphics page 1
// b0: inhibit shadowing, text page 1
//
// The interpretations of how the overlapping high-res and super high-res inhibit
// bits apply used below is taken from The Apple IIgs Technical Reference, P. 178.
// Of course, zones are:
//
// $0400$0800 Text Page 1
// $0800$0C00 Text Page 2 [ROM 03 machines]
// $2000$4000 High-res Page 1, and Super High-res in odd banks
// $4000$6000 High-res Page 2, and Huper High-res in odd banks
// $6000$a000 Odd banks only, rest of Super High-res
// [plus IO and language card space, subject to your definition of shadowing]
constexpr int shadow_shift = 10;
constexpr int auxiliary_offset = 0x10000 >> shadow_shift;
// Text Page 1, main and auxiliary — $0400$0800.
for(size_t c = 0x0400 >> shadow_shift; c < 0x0800 >> shadow_shift; c++) {
shadow_pages[c] = shadow_pages[c+auxiliary_offset] = !(shadow_register_ & 0x01);
}
// Text Page 2, main and auxiliary — 0x08000x0c00.
// TODO: on a ROM03 machine only.
for(size_t c = 0x0800 >> shadow_shift; c < 0x0c00 >> shadow_shift; c++) {
shadow_pages[c] = shadow_pages[c+auxiliary_offset] = !(shadow_register_ & 0x20);
}
// Hi-res graphics Page 1, main and auxiliary — $2000$4000;
// also part of the super high-res graphics page on odd pages.
//
// Even test applied:
// high-res graphics page 1 inhibit bit alone is definitive.
//
// Odd test:
// (high-res graphics inhibit or auxiliary high res graphics inhibit) _and_ (super high-res inhibit).
//
for(size_t c = 0x2000 >> shadow_shift; c < 0x4000 >> shadow_shift; c++) {
shadow_pages[c] = !(shadow_register_ & 0x02);
shadow_pages[c+auxiliary_offset] = !(shadow_register_ & 0x12);
}
// Hi-res graphics Page 2, main and auxiliary — $4000$6000;
// also part of the super high-res graphics page.
//
// Test applied: much like that for page 1.
for(size_t c = 0x4000 >> shadow_shift; c < 0x6000 >> shadow_shift; c++) {
shadow_pages[c] = !(shadow_register_ & 0x04);
shadow_pages[c+auxiliary_offset] = !(shadow_register_ & 0x14);
}
// Residue of Super Hi-Res — $6000$a000 (odd pages only).
for(size_t c = 0x6000 >> shadow_shift; c < 0xa000 >> shadow_shift; c++) {
shadow_pages[c+auxiliary_offset] = !(shadow_register_ & 0x08);
}
}
// Cf. the AuxiliarySwitches; establishes whether main or auxiliary RAM
// is exposed in bank $00 for a bunch of regions.
void set_main_paging() {
const auto state = auxiliary_switches_.main_state();
#define set(page, flags) {\
auto &region = regions[region_map[page]]; \
region.read = flags.read ? &ram_base[0x01'0000] : ram_base; \
region.write = flags.write ? &ram_base[0x01'0000] : ram_base; \
}
// Base: $0200$03FF.
set(0x02, state.base);
assert_is_region(0x02, 0x04);
// Region $0400$07ff.
set(0x04, state.region_04_08);
assert_is_region(0x04, 0x08);
// Base: $0800$1FFF.
set(0x08, state.base);
assert_is_region(0x08, 0x20);
// Region $2000$3FFF.
set(0x20, state.region_20_40);
assert_is_region(0x20, 0x40);
// Base: $4000$BFFF.
set(0x40, state.base);
assert_is_region(0x40, 0xc0);
#undef set
// This also affects shadowing flags, if shadowing is enabled at all,
// and might affect RAM in the IO area of bank $00 because the language
// card can be inhibited on a IIgs.
set_card_paging();
}
void set_all_paging() {
set_card_paging();
set_zero_page_paging(); // ... which calls set_language_card_paging().
set_main_paging();
set_shadowing();
}
void print_state() {
uint8_t region = region_map[0];
uint32_t start = 0;
for(uint32_t top = 0; top < 65536; top++) {
if(region_map[top] == region) continue;
printf("%06x -> %06x\t", start, top << 8);
printf("%c%c\n",
(regions[region_map[top] - 1].flags & Region::Is1Mhz) ? '1' : '-',
(regions[region_map[top] - 1].flags & Region::IsIO) ? 'x' : '-'
);
start = top << 8;
region = region_map[top];
}
}
#undef assert_is_region
public:
// Memory layout here is done via double indirection; the main loop should:
// (i) use the top two bytes of the address to get an index from memory_map_; and
// (ii) use that to index the memory_regions table.
//
// Pointers are eight bytes at the time of writing, so the extra level of indirection
// reduces what would otherwise be a 1.25mb table down to not a great deal more than 64kb.
std::array<uint8_t, 65536> region_map{};
uint8_t *ram_base = nullptr;
uint8_t *shadow_base[2] = {nullptr, nullptr};
static constexpr int shadow_mask[2] = {0xff'ffff, 0x01'ffff};
struct Region {
uint8_t *write = nullptr;
const uint8_t *read = nullptr;
uint8_t flags = 0;
enum Flag: uint8_t {
Is1Mhz = 1 << 0, // Both reads and writes should be synchronised with the 1Mhz clock.
IsIO = 1 << 1, // Indicates that this region should be checked for soft switches, registers, etc.
};
};
// Shadow_pages: divides the final 128kb of memory into 1kb chunks and includes a flag to indicate whether
// each is a potential destination for shadowing.
//
// Shadow_banks: divides the whole 16mb of memory into 128kb chunks and includes a flag to indicate whether
// each is a potential source of shadowing.
std::bitset<128> shadow_pages, shadow_banks;
std::array<Region, 40> regions; // An assert above ensures that this is large enough; there's no
// doctrinal reason for it to be whatever size it is now, just
// adjust as required.
};
// TODO: branching below on region.read/write is predicated on the idea that extra scratch space
// would be less efficient. Verify that?
#define MemoryMapRegion(map, address) map.regions[map.region_map[address >> 8]]
#define IsShadowed(map, region, address) (map.shadow_pages[((&region.write[address] - map.ram_base) >> 10) & 127] & map.shadow_banks[address >> 17])
#define MemoryMapRead(region, address, value) *value = region.read ? region.read[address] : 0xff
#define MemoryMapWrite(map, region, address, value) \
if(region.write) { \
region.write[address] = *value; \
const bool _mm_is_shadowed = IsShadowed(map, region, address); \
map.shadow_base[is_shadowed][(&region.write[address] - map.ram_base) & map.shadow_mask[_mm_is_shadowed]] = *value; \
}
// Quick notes on ::IsShadowed contortions:
//
// The objective is to support shadowing:
// 1. without storing a whole extra pointer, and such that the shadowing flags are orthogonal to the current auxiliary memory settings;
// 2. in such a way as to support shadowing both in banks $00/$01 and elsewhere; and
// 3. to do so without introducing too much in the way of branching.
//
// Hence the implemented solution: if shadowing is enabled then use the distance from the start of physical RAM
// modulo 128k indexed into the bank $e0/$e1 RAM.
//
// With a further twist: the modulo and pointer are indexed on ::IsShadowed to eliminate a branch even on that.
}
}
#endif /* MemoryMap_h */

View File

@ -0,0 +1,375 @@
//
// Sound.cpp
// Clock Signal
//
// Created by Thomas Harte on 04/11/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#include "Sound.hpp"
#include <cassert>
#include <cstdio>
#include <numeric>
// TODO: is it safe not to check for back-pressure in pending_stores_?
using namespace Apple::IIgs::Sound;
GLU::GLU(Concurrency::DeferringAsyncTaskQueue &audio_queue) : audio_queue_(audio_queue) {
// Reset all pending stores.
MemoryWrite disabled_write;
disabled_write.enabled = false;
for(int c = 0; c < StoreBufferSize; c++) {
pending_stores_[c].store(disabled_write);
}
}
void GLU::set_data(uint8_t data) {
if(local_.control & 0x40) {
// RAM access.
local_.ram_[address_] = data;
MemoryWrite write;
write.enabled = true;
write.address = address_;
write.value = data;
write.time = pending_store_write_time_;
pending_stores_[pending_store_write_].store(write, std::memory_order::memory_order_release);
pending_store_write_ = (pending_store_write_ + 1) % (StoreBufferSize - 1);
} else {
// Register access.
const auto address = address_; // To make sure I don't inadvertently 'capture' address_.
local_.set_register(address, data);
audio_queue_.defer([this, address, data] () {
remote_.set_register(address, data);
});
}
if(local_.control & 0x20) {
++address_;
}
}
void GLU::EnsoniqState::set_register(uint16_t address, uint8_t value) {
switch(address & 0xe0) {
case 0x00:
oscillators[address & 0x1f].velocity = uint16_t((oscillators[address & 0x1f].velocity & 0xff00) | (value << 0));
break;
case 0x20:
oscillators[address & 0x1f].velocity = uint16_t((oscillators[address & 0x1f].velocity & 0x00ff) | (value << 8));
break;
case 0x40:
oscillators[address & 0x1f].volume = value;
break;
case 0x60:
/* Does setting the last sample make any sense? */
break;
case 0x80:
oscillators[address & 0x1f].address = value;
break;
case 0xa0: {
oscillators[address & 0x1f].control = value;
// Halt + M0 => reset position.
if((oscillators[address & 0x1f].control & 0x3) == 3) {
oscillators[address & 0x1f].control |= 1;
}
} break;
case 0xc0:
oscillators[address & 0x1f].table_size = value;
// The most-significant bit that should be used is 16 + (value & 7).
oscillators[address & 0x1f].overflow_mask = ~(0xffffff >> (7 - (value & 7)));
break;
default:
switch(address & 0xff) {
case 0xe0:
/* Does setting the interrupt register really make any sense? */
break;
case 0xe1:
oscillator_count = 1 + ((value >> 1) & 31);
break;
case 0xe2:
/* Writing to the analogue to digital input definitely makes no sense. */
break;
}
break;
}
}
uint8_t GLU::get_data() {
const auto address = address_;
if(local_.control & 0x20) {
++address_;
}
switch(address & 0xe0) {
case 0x00: return local_.oscillators[address & 0x1f].velocity & 0xff;
case 0x20: return local_.oscillators[address & 0x1f].velocity >> 8;;
case 0x40: return local_.oscillators[address & 0x1f].volume;
case 0x60: return local_.oscillators[address & 0x1f].sample(local_.ram_); // i.e. look up what the sample was on demand.
case 0x80: return local_.oscillators[address & 0x1f].address;
case 0xa0: return local_.oscillators[address & 0x1f].control;
case 0xc0: return local_.oscillators[address & 0x1f].table_size;
default:
switch(address & 0xff) {
case 0xe0: {
// Find the first enabled oscillator that is signalling an interrupt and has interrupts enabled.
for(int c = 0; c < local_.oscillator_count; c++) {
if(local_.oscillators[c].interrupt_request && (local_.oscillators[c].control & 0x08)) {
local_.oscillators[c].interrupt_request = false;
return uint8_t(0x41 | (c << 1));
}
}
// No interrupt found.
return 0xc1;
} break;
case 0xe1: return uint8_t((local_.oscillator_count - 1) << 1); // TODO: should other bits be 0 or 1?
case 0xe2: return 128; // Input audio. Unimplemented!
}
break;
}
return 0;
}
bool GLU::get_interrupt_line() {
// Return @c true if any oscillator currently has its interrupt request
// set, and has interrupts enabled.
for(int c = 0; c < local_.oscillator_count; c++) {
if(local_.oscillators[c].interrupt_request && (local_.oscillators[c].control & 0x08)) {
return true;
}
}
return false;
}
// MARK: - Time entry points.
void GLU::run_for(Cycles cycles) {
// Update local state, without generating audio.
skip_audio(local_, cycles.as<size_t>());
// Update the timestamp for memory writes;
pending_store_write_time_ += cycles.as<uint32_t>();
}
void GLU::get_samples(std::size_t number_of_samples, std::int16_t *target) {
// Update remote state, generating audio.
generate_audio(number_of_samples, target);
}
void GLU::skip_samples(const std::size_t number_of_samples) {
// Update remote state, without generating audio.
skip_audio(remote_, number_of_samples);
// Apply any pending stores.
std::atomic_thread_fence(std::memory_order::memory_order_acquire);
const uint32_t final_time = pending_store_read_time_ + uint32_t(number_of_samples);
while(true) {
auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire);
if(!next_store.enabled) break;
if(next_store.time >= final_time) break;
remote_.ram_[next_store.address] = next_store.value;
next_store.enabled = false;
pending_stores_[pending_store_read_].store(next_store, std::memory_order::memory_order_relaxed);
pending_store_read_ = (pending_store_read_ + 1) & (StoreBufferSize - 1);
}
}
void GLU::set_sample_volume_range(std::int16_t range) {
output_range_ = range;
}
// MARK: - Interface boilerplate.
void GLU::set_control(uint8_t control) {
local_.control = control;
audio_queue_.defer([this, control] () {
remote_.control = control;
});
}
uint8_t GLU::get_control() {
return local_.control;
}
void GLU::set_address_low(uint8_t low) {
address_ = uint16_t((address_ & 0xff00) | low);
}
uint8_t GLU::get_address_low() {
return address_ & 0xff;
}
void GLU::set_address_high(uint8_t high) {
address_ = uint16_t((high << 8) | (address_ & 0x00ff));
}
uint8_t GLU::get_address_high() {
return address_ >> 8;
}
// MARK: - Update logic.
Cycles GLU::get_next_sequence_point() const {
uint32_t result = std::numeric_limits<decltype(result)>::max();
for(int c = 0; c < local_.oscillator_count; c++) {
// Don't do anything for halted oscillators, or for oscillators that can't hit stops.
if((local_.oscillators[c].control&3) != 2) {
continue;
}
// Determine how many cycles until a stop is hit and update the pending result
// if this is the new soonest-to-expire oscillator.
const auto first_overflow_value = (local_.oscillators[c].overflow_mask - 1) << 1;
const auto time_until_stop = (first_overflow_value - local_.oscillators[c].position + local_.oscillators[c].velocity - 1) / local_.oscillators[c].velocity;
result = std::min(result, time_until_stop);
}
return Cycles(result);
}
void GLU::skip_audio(EnsoniqState &state, size_t number_of_samples) {
// Just advance all oscillator pointers and check for interrupts.
// If a read occurs to the current-output level, generate it then.
for(int c = 0; c < state.oscillator_count; c++) {
// Don't do anything for halted oscillators.
if(state.oscillators[c].control&1) continue;
// Update phase.
state.oscillators[c].position += state.oscillators[c].velocity * number_of_samples;
// Check for stops, and any interrupts that therefore flow.
if((state.oscillators[c].control & 2) && (state.oscillators[c].position & state.oscillators[c].overflow_mask)) {
// Apply halt, set interrupt request flag.
state.oscillators[c].position = 0;
state.oscillators[c].control |= 1;
state.oscillators[c].interrupt_request = true;
}
}
}
void GLU::generate_audio(size_t number_of_samples, std::int16_t *target) {
auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire);
uint8_t next_amplitude = 255;
for(size_t sample = 0; sample < number_of_samples; sample++) {
// TODO: there's a bit of a hack here where it is assumed that the input clock has been
// divided in advance. Real hardware divides by 8, I think?
// Seed output as 0.
int output = 0;
// Apply phase updates to all enabled oscillators.
for(int c = 0; c < remote_.oscillator_count; c++) {
// Don't do anything for halted oscillators.
if(remote_.oscillators[c].control&1) continue;
remote_.oscillators[c].position += remote_.oscillators[c].velocity;
// Test for a new halting event.
switch(remote_.oscillators[c].control & 6) {
case 0: // Free-run mode; don't truncate the position at all, in case the
// accumulator bits in use changes.
output += remote_.oscillators[c].output(remote_.ram_);
break;
case 2: // One-shot mode; check for end of run. Otherwise update sample.
if(remote_.oscillators[c].position & remote_.oscillators[c].overflow_mask) {
remote_.oscillators[c].position = 0;
remote_.oscillators[c].control |= 1;
}
break;
case 4: // Sync/AM mode.
if(c&1) {
// Oscillator is odd-numbered; it will amplitude-modulate the next voice.
next_amplitude = remote_.oscillators[c].sample(remote_.ram_);
continue;
} else {
// Oscillator is even-numbered; it will 'sync' to the even voice, i.e. any
// time it wraps around, it will reset the next oscillator.
if(remote_.oscillators[c].position & remote_.oscillators[c].overflow_mask) {
remote_.oscillators[c].position &= remote_.oscillators[c].overflow_mask;
remote_.oscillators[c+1].position = 0;
}
}
break;
case 6: // Swap mode; possibly trigger partner, and update sample.
// Per tech note #11: "Whenever a swap occurs from a higher-numbered
// oscillator to a lower-numbered one, the output signal from the corresponding
// generator temporarily falls to the zero-crossing level (silence)"
if(remote_.oscillators[c].position & remote_.oscillators[c].overflow_mask) {
remote_.oscillators[c].control |= 1;
remote_.oscillators[c].position = 0;
remote_.oscillators[c^1].control &= ~1;
}
break;
}
// Don't add output for newly-halted oscillators.
if(remote_.oscillators[c].control&1) continue;
// Append new output.
output += (remote_.oscillators[c].output(remote_.ram_) * next_amplitude) / 255;
next_amplitude = 255;
}
// Maximum total output was 32 channels times a 16-bit range. Map that down.
// TODO: dynamic total volume?
target[sample] = (output * output_range_) >> 20;
// Apply any RAM writes that interleave here.
++pending_store_read_time_;
if(!next_store.enabled) continue;
if(next_store.time != pending_store_read_time_) continue;
remote_.ram_[next_store.address] = next_store.value;
next_store.enabled = false;
pending_stores_[pending_store_read_].store(next_store, std::memory_order::memory_order_relaxed);
pending_store_read_ = (pending_store_read_ + 1) & (StoreBufferSize - 1);
}
}
uint8_t GLU::EnsoniqState::Oscillator::sample(uint8_t *ram) {
// Determines how many you'd have to shift a 16-bit pointer to the right for,
// in order to hit only the position-supplied bits.
const int pointer_shift = 8 - ((table_size >> 3) & 7);
// Table size mask should be 0x8000 for the largest table size, and 0xff00 for
// the smallest.
const uint16_t table_size_mask = 0xffff >> pointer_shift;
// The pointer should use (at most) 15 bits; starting with bit 1 for resolution 0
// and starting at bit 8 for resolution 7.
const uint16_t table_pointer = uint16_t(position >> ((table_size&7) + pointer_shift));
// The full pointer is composed of the bits of the programmed address not touched by
// the table pointer, plus the table pointer.
const uint16_t sample_address = ((address << 8) & ~table_size_mask) | (table_pointer & table_size_mask);
// Ignored here: bit 6 should select between RAM banks. But for now this is IIgs-centric,
// and that has only one bank of RAM.
return ram[sample_address];
}
int16_t GLU::EnsoniqState::Oscillator::output(uint8_t *ram) {
const auto level = sample(ram);
// "An oscillator will halt when a zero is encountered in its waveform table."
// TODO: only if in free-run mode, I think? Or?
if(!level) {
control |= 1;
return 0;
}
// Samples are unsigned 8-bit; do the proper work to make volume work correctly.
return int8_t(level ^ 128) * volume;
}

View File

@ -0,0 +1,111 @@
//
// Sound.hpp
// Clock Signal
//
// Created by Thomas Harte on 04/11/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef Apple_IIgs_Sound_hpp
#define Apple_IIgs_Sound_hpp
#include <atomic>
#include "../../../ClockReceiver/ClockReceiver.hpp"
#include "../../../Concurrency/AsyncTaskQueue.hpp"
#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp"
namespace Apple {
namespace IIgs {
namespace Sound {
class GLU: public Outputs::Speaker::SampleSource {
public:
GLU(Concurrency::DeferringAsyncTaskQueue &audio_queue);
void set_control(uint8_t);
uint8_t get_control();
void set_data(uint8_t);
uint8_t get_data();
void set_address_low(uint8_t);
uint8_t get_address_low();
void set_address_high(uint8_t);
uint8_t get_address_high();
void run_for(Cycles);
Cycles get_next_sequence_point() const;
bool get_interrupt_line();
// SampleSource.
void get_samples(std::size_t number_of_samples, std::int16_t *target);
void set_sample_volume_range(std::int16_t range);
void skip_samples(const std::size_t number_of_samples);
private:
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
uint16_t address_ = 0;
// Use a circular buffer for piping memory alterations onto the audio
// thread; it would be prohibitive to defer every write individually.
//
// Assumed: on most modern architectures, an atomic 64-bit read or
// write can be achieved locklessly.
struct MemoryWrite {
uint32_t time;
uint16_t address;
uint8_t value;
bool enabled;
};
static_assert(sizeof(MemoryWrite) == 8);
constexpr static int StoreBufferSize = 16384;
std::atomic<MemoryWrite> pending_stores_[StoreBufferSize];
uint32_t pending_store_read_ = 0, pending_store_read_time_ = 0;
uint32_t pending_store_write_ = 0, pending_store_write_time_ = 0;
// Maintain state both 'locally' (i.e. on the emulation thread) and
// 'remotely' (i.e. on the audio thread).
struct EnsoniqState {
uint8_t ram_[65536];
struct Oscillator {
uint32_t position;
// Programmer-set values.
uint16_t velocity;
uint8_t volume;
uint8_t address;
uint8_t control;
uint8_t table_size;
// Derived state.
uint32_t overflow_mask; // If a non-zero bit gets anywhere into the overflow mask, this channel
// has wrapped around. It's a function of table_size.
bool interrupt_request = false; // Will be non-zero if this channel would request an interrupt, were
// it currently enabled to do so.
uint8_t sample(uint8_t *ram);
int16_t output(uint8_t *ram);
} oscillators[32];
// Some of these aren't actually needed on both threads.
uint8_t control = 0;
int oscillator_count = 1;
void set_register(uint16_t address, uint8_t value);
} local_, remote_;
// Functions to update an EnsoniqState; these don't belong to the state itself
// because they also access the pending stores (inter alia).
void generate_audio(size_t number_of_samples, std::int16_t *target);
void skip_audio(EnsoniqState &state, size_t number_of_samples);
// Audio-thread state.
int16_t output_range_ = 0;
};
}
}
}
#endif /* SoundGLU_hpp */

View File

@ -0,0 +1,810 @@
//
// Video.cpp
// Clock Signal
//
// Created by Thomas Harte on 31/10/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#include "Video.hpp"
using namespace Apple::IIgs::Video;
namespace {
constexpr int CyclesPerTick = 7; // One 'tick' being the non-stretched length of a cycle on the old Apple II 1Mhz clock.
constexpr int CyclesPerLine = 456; // Each of the Mega II's cycles lasts 7 cycles, making 455/line except for the
// final on on a line which lasts an additional 1 (i.e. is 1/7th longer).
constexpr int Lines = 262;
constexpr int FinalPixelLine = 192;
constexpr auto FinalColumn = CyclesPerLine / CyclesPerTick;
// Converts from Apple's RGB ordering to this emulator's.
#if TARGET_RT_BIG_ENDIAN
#define PaletteConvulve(x) uint16_t(x)
#else
#define PaletteConvulve(x) uint16_t(((x&0xf00) >> 8) | ((x&0x0ff) << 8))
#endif
// The 12-bit values used by the Apple IIgs to approximate Apple II colours,
// as implied by tech note #63's use of them as border colours.
// http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.063.html
constexpr uint16_t appleii_palette[16] = {
PaletteConvulve(0x0000), // Black.
PaletteConvulve(0x0d03), // Deep Red.
PaletteConvulve(0x0009), // Dark Blue.
PaletteConvulve(0x0d2d), // Purple.
PaletteConvulve(0x0072), // Dark Green.
PaletteConvulve(0x0555), // Dark Gray.
PaletteConvulve(0x022f), // Medium Blue.
PaletteConvulve(0x06af), // Light Blue.
PaletteConvulve(0x0850), // Brown.
PaletteConvulve(0x0f60), // Orange.
PaletteConvulve(0x0aaa), // Light Grey.
PaletteConvulve(0x0f98), // Pink.
PaletteConvulve(0x01d0), // Light Green.
PaletteConvulve(0x0ff0), // Yellow.
PaletteConvulve(0x04f9), // Aquamarine.
PaletteConvulve(0x0fff), // White.
};
// Reasoned guesswork ahoy!
//
// The IIgs VGC can fetch four bytes per column — I'm unclear physically how, but that's definitely true
// since the IIgs modes packs 160 bytes work of graphics into the Apple II's usual 40-cycle fetch area;
// it's possible that if I understood the meaning of the linear video bit in the new video flag I'd know more.
//
// Super Hi-Res also fetches 16*2 = 32 bytes of palette and a control byte sometime before each row.
// So it needs five windows for that.
//
// Guessing four cycles of sync, I've chosen to arrange one output row for this emulator as:
//
// 5 cycles of back porch; [TODO: include a colour burst]
// 8 windows left border, the final five of which fetch palette and control if in IIgs mode;
// 40 windows of pixel output;
// 8 cycles of right border;
// 4 cycles of sync (including the extra 1/7th window, as it has to go _somewhere_).
//
// Otherwise, the first 200 rows may be pixels and the 192 in the middle of those are the II set.
constexpr int first_sync_line = 220; // A complete guess. Information needed.
constexpr int blank_ticks = 5;
constexpr int left_border_ticks = 8;
constexpr int pixel_ticks = 40;
constexpr int right_border_ticks = 8;
constexpr int start_of_left_border = blank_ticks;
constexpr int start_of_pixels = start_of_left_border + left_border_ticks;
constexpr int start_of_right_border = start_of_pixels + pixel_ticks;
constexpr int start_of_sync = start_of_right_border + right_border_ticks;
constexpr int sync_period = CyclesPerLine - start_of_sync*CyclesPerTick;
// I have made the guess that this occurs when the Mega II horizontal counter rolls over.
// This is just a guess.
constexpr int megaii_interrupt_point = 192*CyclesPerLine + (start_of_pixels - 28)*CyclesPerTick - 2;
// A table to map from 7-bit integers to 14-bit versions with all bits doubled.
constexpr uint16_t double_bytes[128] = {
0x0000, 0x0003, 0x000c, 0x000f, 0x0030, 0x0033, 0x003c, 0x003f,
0x00c0, 0x00c3, 0x00cc, 0x00cf, 0x00f0, 0x00f3, 0x00fc, 0x00ff,
0x0300, 0x0303, 0x030c, 0x030f, 0x0330, 0x0333, 0x033c, 0x033f,
0x03c0, 0x03c3, 0x03cc, 0x03cf, 0x03f0, 0x03f3, 0x03fc, 0x03ff,
0x0c00, 0x0c03, 0x0c0c, 0x0c0f, 0x0c30, 0x0c33, 0x0c3c, 0x0c3f,
0x0cc0, 0x0cc3, 0x0ccc, 0x0ccf, 0x0cf0, 0x0cf3, 0x0cfc, 0x0cff,
0x0f00, 0x0f03, 0x0f0c, 0x0f0f, 0x0f30, 0x0f33, 0x0f3c, 0x0f3f,
0x0fc0, 0x0fc3, 0x0fcc, 0x0fcf, 0x0ff0, 0x0ff3, 0x0ffc, 0x0fff,
0x3000, 0x3003, 0x300c, 0x300f, 0x3030, 0x3033, 0x303c, 0x303f,
0x30c0, 0x30c3, 0x30cc, 0x30cf, 0x30f0, 0x30f3, 0x30fc, 0x30ff,
0x3300, 0x3303, 0x330c, 0x330f, 0x3330, 0x3333, 0x333c, 0x333f,
0x33c0, 0x33c3, 0x33cc, 0x33cf, 0x33f0, 0x33f3, 0x33fc, 0x33ff,
0x3c00, 0x3c03, 0x3c0c, 0x3c0f, 0x3c30, 0x3c33, 0x3c3c, 0x3c3f,
0x3cc0, 0x3cc3, 0x3ccc, 0x3ccf, 0x3cf0, 0x3cf3, 0x3cfc, 0x3cff,
0x3f00, 0x3f03, 0x3f0c, 0x3f0f, 0x3f30, 0x3f33, 0x3f3c, 0x3f3f,
0x3fc0, 0x3fc3, 0x3fcc, 0x3fcf, 0x3ff0, 0x3ff3, 0x3ffc, 0x3fff,
};
}
Video::Video() :
VideoSwitches<Cycles>(true, Cycles(2), [this] (Cycles cycles) { advance(cycles); }),
crt_(CyclesPerLine - 1, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red4Green4Blue4) {
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
crt_.set_visible_area(Outputs::Display::Rect(0.097f, 0.1f, 0.85f, 0.85f));
// Establish the shift lookup table for NTSC -> RGB output.
for(size_t c = 0; c < sizeof(ntsc_delay_lookup_) / sizeof(*ntsc_delay_lookup_); c++) {
const auto old_delay = c >> 2;
// If delay is 3, 2, 1 or 0 the output is just that minus 1.
// Otherwise the output is either still 4, or 3 if the two lowest bits don't match.
if(old_delay < 4) {
ntsc_delay_lookup_[c] = (old_delay > 0) ? uint8_t(old_delay - 1) : 4;
} else {
ntsc_delay_lookup_[c] = (c&1) == ((c >> 1)&1) ? 4 : 3;
}
ntsc_delay_lookup_[c] = 4;
}
}
void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
return crt_.get_scaled_scan_status();
}
void Video::set_display_type(Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
Outputs::Display::DisplayType Video::get_display_type() const {
return crt_.get_display_type();
}
void Video::set_internal_ram(const uint8_t *ram) {
ram_ = ram;
}
void Video::advance(Cycles cycles) {
const int next_cycles_into_frame = cycles_into_frame_ + cycles.as<int>();
// Check for Mega II-style interrupt sources, prior to updating cycles_into_frame_.
if(cycles_into_frame_ < megaii_interrupt_point && next_cycles_into_frame >= megaii_interrupt_point) {
++megaii_frame_counter_;
megaii_interrupt_state_ |= 0x08 | (megaii_frame_counter_ & 0x10);
megaii_frame_counter_ &= 15;
// The "quarter second interrupt" is also called the "3.75Hz interrupt" elsewhere.
// So trigger it every 16 frames.
}
// Update video output.
const int column_start = (cycles_into_frame_ % CyclesPerLine) / CyclesPerTick;
const int row_start = cycles_into_frame_ / CyclesPerLine;
cycles_into_frame_ = next_cycles_into_frame % (CyclesPerLine * Lines);
const int column_end = (cycles_into_frame_ % CyclesPerLine) / CyclesPerTick;
const int row_end = cycles_into_frame_ / CyclesPerLine;
if(row_end == row_start) {
if(column_end != column_start) {
output_row(row_start, column_start, column_end);
}
} else {
if(column_start != FinalColumn) {
output_row(row_start, column_start, FinalColumn);
}
for(int row = row_start+1; row < row_end; row++) {
output_row(row, 0, FinalColumn);
}
if(column_end) {
output_row(row_end, 0, column_end);
}
}
}
Cycles Video::get_next_sequence_point() const {
const int cycles_into_row = cycles_into_frame_ % CyclesPerLine;
const int row = cycles_into_frame_ / CyclesPerLine;
constexpr int sequence_point_offset = (blank_ticks + left_border_ticks) * CyclesPerTick;
// Seed as the distance to the next row 0.
int result = CyclesPerLine + sequence_point_offset - cycles_into_row + (Lines - row - 1)*CyclesPerLine;
// Replace with the start of the next line, if closer.
if(row <= 200) {
if(cycles_into_row < sequence_point_offset) return Cycles(sequence_point_offset - cycles_into_row);
if(row < 200) result = CyclesPerLine + sequence_point_offset - cycles_into_row;
}
// Replace with the next Mega II interrupt point if those are enabled and it is sooner.
if(megaii_interrupt_mask_) {
const int time_until_megaii = megaii_interrupt_point - cycles_into_frame_;
if(time_until_megaii > 0 && time_until_megaii < result) {
result = time_until_megaii;
}
}
return Cycles(result);
}
void Video::output_row(int row, int start, int end) {
// Deal with vertical sync.
if(row >= first_sync_line && row < first_sync_line + 3) {
// Simplification: just output the whole line at line's end.
if(end == FinalColumn) {
crt_.output_sync(CyclesPerLine - sync_period);
crt_.output_blank(sync_period);
}
return;
}
// Pixel or pure border => blank as usual.
// Output blank only at the end of its window.
if(start < blank_ticks && end >= blank_ticks) {
crt_.output_blank(blank_ticks * CyclesPerTick);
start = blank_ticks;
if(start == end) return;
}
// The pixel buffer will actually be allocated a column early, to allow double high/low res to start
// half a column before everything else.
constexpr int pixel_buffer_allocation = start_of_pixels - 1;
// Possibly output border, pixels, border, if this is a pixel line.
if(row < 192 + ((new_video_&0x80) >> 4)) { // i.e. 192 lines for classic Apple II video, 200 for IIgs video.
// Output left border as far as currently known.
if(start >= start_of_left_border && start < pixel_buffer_allocation) {
const int end_of_period = std::min(pixel_buffer_allocation, end);
if(border_colour_) {
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
if(pixel) *pixel = border_colour_;
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
} else {
crt_.output_blank((end_of_period - start) * CyclesPerTick);
}
start = end_of_period;
if(start == end) return;
}
assert(end > start);
// Fetch and output such pixels as it is time for.
if(start >= pixel_buffer_allocation && start < start_of_right_border) {
const int end_of_period = std::min(start_of_right_border, end);
const auto mode = graphics_mode(row);
if(start == pixel_buffer_allocation) {
// YUCKY HACK. I do not know when the IIgs fetches its super high-res palette
// and control byte. Since I do not know, any guess is equally likely negatively
// to affect software. Therefore this hack is as good as any other guess:
// assume RAM has magical burst bandwidth, and fetch the whole set instantly.
// I could spread this stuff out to allow for real bandwidth, but it'd likely be
// no more accurate, while having less of an obvious I-HACKED-THIS red flag attached.
line_control_ = ram_[0x19d00 + row];
const int palette_base = (line_control_ & 15) * 32 + 0x19e00;
for(int c = 0; c < 16; c++) {
const int entry = ram_[palette_base + (c << 1)] | (ram_[palette_base + (c << 1) + 1] << 8);
palette_[c] = PaletteConvulve(entry);
}
// Post an interrupt if requested.
if(line_control_ & 0x40) {
set_interrupts(0x20);
}
// Set up appropriately for fill mode (or not).
for(int c = 0; c < 4; c++) {
palette_zero_[c] = (line_control_ & 0x20) ? &palette_[c * 4] : &palette_throwaway_;
}
// Reset NTSC decoding and total line buffering.
ntsc_delay_ = 4;
pixels_start_column_ = start;
}
if(!next_pixel_ || pixels_format_ != format_for_mode(mode)) {
// Flush anything already in a buffer.
if(pixels_start_column_ < start) {
crt_.output_data((start - pixels_start_column_) * CyclesPerTick, next_pixel_ ? size_t(next_pixel_ - pixels_) : 1);
next_pixel_ = pixels_ = nullptr;
}
// Allocate a new buffer; 640 plus one column is as bad as it gets.
// TODO: make proper size estimate?
next_pixel_ = pixels_ = reinterpret_cast<uint16_t *>(crt_.begin_data(656, 2));
pixels_start_column_ = start;
pixels_format_ = format_for_mode(mode);
}
if(next_pixel_) {
int window_start = start - start_of_pixels;
const int window_end = end_of_period - start_of_pixels;
// Fill in border colour if this is the first column.
if(window_start == -1) {
if(next_pixel_) {
int extra_border_length;
switch(mode) {
case GraphicsMode::DoubleText:
case GraphicsMode::Text:
case GraphicsMode::DoubleHighRes:
case GraphicsMode::DoubleLowRes:
case GraphicsMode::DoubleHighResMono:
extra_border_length = 7;
break;
case GraphicsMode::HighRes:
case GraphicsMode::LowRes:
case GraphicsMode::FatLowRes:
extra_border_length = 14;
break;
case GraphicsMode::SuperHighRes:
extra_border_length = (line_control_ & 0x80) ? 16 : 8;
break;
default: // Unreachable.
extra_border_length = 0;
break;
}
for(int c = 0; c < extra_border_length; c++) {
next_pixel_[c] = border_colour_;
}
next_pixel_ += extra_border_length;
}
++window_start;
if(window_start == window_end) return;
}
switch(mode) {
case GraphicsMode::SuperHighRes:
next_pixel_ = output_super_high_res(next_pixel_, window_start, window_end, row);
break;
case GraphicsMode::Text:
next_pixel_ = output_text(next_pixel_, window_start, window_end, row);
break;
case GraphicsMode::DoubleText:
next_pixel_ = output_double_text(next_pixel_, window_start, window_end, row);
break;
case GraphicsMode::FatLowRes:
next_pixel_ = output_fat_low_resolution(next_pixel_, window_start, window_end, row);
break;
case GraphicsMode::LowRes:
next_pixel_ = output_low_resolution(next_pixel_, window_start, window_end, row);
break;
case GraphicsMode::DoubleLowRes:
next_pixel_ = output_double_low_resolution(next_pixel_, window_start, window_end, row);
break;
case GraphicsMode::HighRes:
next_pixel_ = output_high_resolution(next_pixel_, window_start, window_end, row);
break;
case GraphicsMode::DoubleHighRes:
next_pixel_ = output_double_high_resolution(next_pixel_, window_start, window_end, row);
break;
case GraphicsMode::DoubleHighResMono:
next_pixel_ = output_double_high_resolution_mono(next_pixel_, window_start, window_end, row);
break;
default:
assert(false); // i.e. other modes yet to do.
}
}
if(end_of_period == start_of_right_border) {
// Flush what remains in the NTSC queue, if applicable.
// TODO: with real NTSC test, why not?
if(next_pixel_ && is_colour_ntsc(mode)) {
ntsc_shift_ >>= 14;
next_pixel_ = output_shift(next_pixel_, 81);
}
crt_.output_data((start_of_right_border - pixels_start_column_) * CyclesPerTick, next_pixel_ ? size_t(next_pixel_ - pixels_) : 1);
next_pixel_ = pixels_ = nullptr;
}
start = end_of_period;
if(start == end) return;
}
assert(end > start);
// Output right border as far as currently known.
if(start >= start_of_right_border && start < start_of_sync) {
const int end_of_period = std::min(start_of_sync, end);
if(border_colour_) {
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
if(pixel) *pixel = border_colour_;
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
} else {
crt_.output_blank((end_of_period - start) * CyclesPerTick);
}
// There's no point updating start here; just fall
// through to the end == FinalColumn test.
}
} else {
// This line is all border, all the time.
if(start >= start_of_left_border && start < start_of_sync) {
const int end_of_period = std::min(start_of_sync, end);
if(border_colour_) {
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
if(pixel) *pixel = border_colour_;
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
} else {
crt_.output_blank((end_of_period - start) * CyclesPerTick);
}
start = end_of_period;
if(start == end) return;
}
}
// Output sync if the moment has arrived.
if(end == FinalColumn) {
crt_.output_sync(sync_period);
}
}
bool Video::get_is_vertical_blank(Cycles offset) {
// Cf. http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.040.html ;
// this bit covers the entire vertical border area, not just the NTSC-sense vertical blank,
// and considers the border to begin at 192 even though Super High-res mode is 200 lines.
return (cycles_into_frame_ + offset.as<int>())%(Lines * CyclesPerLine) >= FinalPixelLine * CyclesPerLine;
}
Video::Counters Video::get_counters(Cycles offset) {
// Tech note #39:
//
// "The seven-bit horizontal counter starts at $00 and counts from $40 to $7F (the sequence
// is $00, $40, $41,...,$7E, $7F, $00, $40,...). The active video time consists of 40 one
// microsecond clock cycles starting with $58 and ending with $7F."
//
// "The nine-bit vertical counter ranges from $FA through $1FF (250 through 511) in NTSC mode
// (vertical line count of 262) and from $C8 through $1FF (200 through 511) in PAL video timing
// mode (vertical line count of 312). Vertical counter value $100 corresponds to scan line
// zero in NTSC mode."
// Work out the internal offset into frame; a modulo willoccur momentarily...
auto cycles_into_frame = cycles_into_frame_ + offset.as<int>();
// Nudge slightly so that the regular start of line matches mine.
// TODO: reorient my drawing around the native offsets?
cycles_into_frame = (cycles_into_frame + 25 - start_of_pixels)%(Lines * CyclesPerLine);
// Break it down.
const auto cycles_into_line = (cycles_into_frame % CyclesPerLine) / CyclesPerTick;
auto lines_into_frame = cycles_into_frame / CyclesPerLine;
lines_into_frame += 0x100;
return Counters(
lines_into_frame - ((lines_into_frame / 0x200) * 0x106), // TODO: this assumes NTSC.
cycles_into_line + bool(cycles_into_line) * 0x40);
}
uint8_t Video::get_horizontal_counter(Cycles offset) {
const auto counters = get_counters(offset);
return uint8_t(counters.horizontal | (counters.vertical << 7));
}
uint8_t Video::get_vertical_counter(Cycles offset) {
const auto counters = get_counters(offset);
return uint8_t(counters.vertical >> 1);
}
void Video::set_new_video(uint8_t new_video) {
new_video_ = new_video;
}
uint8_t Video::get_new_video() {
return new_video_;
}
void Video::clear_interrupts(uint8_t mask) {
set_interrupts(interrupts_ & ~(mask & 0x60));
}
void Video::set_interrupt_register(uint8_t mask) {
set_interrupts(interrupts_ | (mask & 0x6));
}
uint8_t Video::get_interrupt_register() {
return interrupts_;
}
bool Video::get_interrupt_line() {
return (interrupts_&0x80) || (megaii_interrupt_mask_&megaii_interrupt_state_);
}
void Video::set_megaii_interrupts_enabled(uint8_t mask) {
megaii_interrupt_mask_ = mask;
}
uint8_t Video::get_megaii_interrupt_status() {
return megaii_interrupt_state_ | (get_annunciator_3() ? 0x20 : 0x00);
}
void Video::clear_megaii_interrupts() {
megaii_interrupt_state_ = 0;
}
void Video::notify_clock_tick() {
set_interrupts(interrupts_ | 0x40);
}
void Video::set_interrupts(uint8_t new_value) {
interrupts_ = new_value & 0x7f;
if((interrupts_ >> 4) & interrupts_ & 0x6)
interrupts_ |= 0x80;
}
void Video::set_border_colour(uint8_t colour) {
border_colour_entry_ = colour & 0x0f;
border_colour_ = appleii_palette[border_colour_entry_];
}
uint8_t Video::get_border_colour() {
return border_colour_entry_;
}
void Video::set_text_colour(uint8_t colour) {
text_colour_entry_ = colour;
text_colour_ = appleii_palette[colour >> 4];
background_colour_ = appleii_palette[colour & 0xf];
}
uint8_t Video::get_text_colour() {
return text_colour_entry_;
}
void Video::set_composite_is_colour(bool) {
// TODO.
}
bool Video::get_composite_is_colour() {
return true;
}
// MARK: - Outputters.
uint16_t *Video::output_char(uint16_t *target, uint8_t source, int row) const {
const int character = source & character_zones_[source >> 6].address_mask;
const uint8_t xor_mask = character_zones_[source >> 6].xor_mask;
const std::size_t character_address = size_t(character << 3) + (row & 7);
const uint8_t character_pattern = character_rom_[character_address] ^ xor_mask;
const uint16_t colours[2] = {background_colour_, text_colour_};
target[0] = colours[(character_pattern & 0x40) >> 6];
target[1] = colours[(character_pattern & 0x20) >> 5];
target[2] = colours[(character_pattern & 0x10) >> 4];
target[3] = colours[(character_pattern & 0x08) >> 3];
target[4] = colours[(character_pattern & 0x04) >> 2];
target[5] = colours[(character_pattern & 0x02) >> 1];
target[6] = colours[(character_pattern & 0x01) >> 0];
return target + 7;
}
uint16_t *Video::output_text(uint16_t *target, int start, int end, int row) const {
const uint16_t row_address = get_row_address(row);
for(int c = start; c < end; c++) {
target = output_char(target, ram_[row_address + c], row);
}
return target;
}
uint16_t *Video::output_double_text(uint16_t *target, int start, int end, int row) const {
const uint16_t row_address = get_row_address(row);
for(int c = start; c < end; c++) {
target = output_char(target, ram_[0x10000 + row_address + c], row);
target = output_char(target, ram_[row_address + c], row);
}
return target;
}
uint16_t *Video::output_super_high_res(uint16_t *target, int start, int end, int row) const {
const int row_address = row * 160 + 0x12000;
// The palette_zero_ writes ensure that palette colour 0 is replaced by whatever was last output,
// if fill mode is enabled. Otherwise they go to throwaway storage.
if(line_control_ & 0x80) {
for(int c = start * 4; c < end * 4; c++) {
const uint8_t source = ram_[row_address + c];
*palette_zero_[3] = target[0] = palette_[0x8 + ((source >> 6) & 0x3)];
*palette_zero_[0] = target[1] = palette_[0xc + ((source >> 4) & 0x3)];
*palette_zero_[1] = target[2] = palette_[0x0 + ((source >> 2) & 0x3)];
*palette_zero_[2] = target[3] = palette_[0x4 + ((source >> 0) & 0x3)];
target += 4;
}
} else {
for(int c = start * 4; c < end * 4; c++) {
const uint8_t source = ram_[row_address + c];
*palette_zero_[0] = target[0] = palette_[(source >> 4) & 0xf];
*palette_zero_[0] = target[1] = palette_[source & 0xf];
target += 2;
}
}
return target;
}
uint16_t *Video::output_double_high_resolution_mono(uint16_t *target, int start, int end, int row) {
const uint16_t row_address = get_row_address(row);
constexpr uint16_t colours[] = {0, 0xffff};
for(int c = start; c < end; c++) {
const uint8_t source[2] = {
ram_[0x10000 + row_address + c],
ram_[row_address + c],
};
target[0] = colours[(source[1] >> 0) & 0x1];
target[1] = colours[(source[1] >> 1) & 0x1];
target[2] = colours[(source[1] >> 2) & 0x1];
target[3] = colours[(source[1] >> 3) & 0x1];
target[4] = colours[(source[1] >> 4) & 0x1];
target[5] = colours[(source[1] >> 5) & 0x1];
target[6] = colours[(source[1] >> 6) & 0x1];
target[7] = colours[(source[0] >> 0) & 0x1];
target[8] = colours[(source[0] >> 1) & 0x1];
target[9] = colours[(source[0] >> 2) & 0x1];
target[10] = colours[(source[0] >> 3) & 0x1];
target[11] = colours[(source[0] >> 4) & 0x1];
target[12] = colours[(source[0] >> 5) & 0x1];
target[13] = colours[(source[0] >> 6) & 0x1];
target += 14;
}
return target;
}
uint16_t *Video::output_low_resolution(uint16_t *target, int start, int end, int row) {
const int row_shift = row&4;
const uint16_t row_address = get_row_address(row);
for(int c = start; c < end; c++) {
const uint8_t source = (ram_[row_address + c] >> row_shift) & 0xf;
// Convulve input as a function of odd/even row.
uint32_t long_source;
if(c&1) {
long_source = uint32_t((source >> 2) | (source << 2) | (source << 6) | (source << 10));
} else {
long_source = uint32_t((source | (source << 4) | (source << 8) | (source << 12)) & 0x3fff);
}
ntsc_shift_ = (long_source << 18) | (ntsc_shift_ >> 14);
target = output_shift(target, 1 + c*2);
}
return target;
}
uint16_t *Video::output_fat_low_resolution(uint16_t *target, int start, int end, int row) {
const int row_shift = row&4;
const uint16_t row_address = get_row_address(row);
for(int c = start; c < end; c++) {
const uint32_t doubled_source = uint32_t(double_bytes[(ram_[row_address + c] >> row_shift) & 0xf]);
const uint32_t long_source = doubled_source | (doubled_source << 8);
// TODO: verify the above.
ntsc_shift_ = (long_source << 18) | (ntsc_shift_ >> 14);
target = output_shift(target, 1 + c*2);
}
return target;
}
uint16_t *Video::output_double_low_resolution(uint16_t *target, int start, int end, int row) {
const int row_shift = row&4;
const uint16_t row_address = get_row_address(row);
for(int c = start; c < end; c++) {
const uint8_t source[2] = {
uint8_t((ram_[row_address + c] >> row_shift) & 0xf),
uint8_t((ram_[0x10000 + row_address + c] >> row_shift) & 0xf)
};
// Convulve input as a function of odd/even row; this is very much like low
// resolution mode except that the first 7 bits to be output will come from
// source[1] and the next 7 from source[0]. Also shifting is offset by
// half a window compared to regular low resolution, so the conditional
// works the other way around.
uint32_t long_source;
if(c&1) {
long_source = uint32_t((source[1] | ((source[1] << 4) & 0x70) | ((source[0] << 4) & 0x80) | (source[0] << 8) | (source[0] << 12)) & 0x3fff);
} else {
long_source = uint32_t((source[1] >> 2) | (source[1] << 2) | ((source[1] << 6) & 0x40) | ((source[0] << 6) & 0x380) | (source[0] << 10));
}
ntsc_shift_ = (long_source << 18) | (ntsc_shift_ >> 14);
target = output_shift(target, c*2);
}
return target;
}
uint16_t *Video::output_high_resolution(uint16_t *target, int start, int end, int row) {
const uint16_t row_address = get_row_address(row);
for(int c = start; c < end; c++) {
uint8_t source = ram_[row_address + c];
const uint32_t doubled_source = uint32_t(double_bytes[source & 0x7f]);
// Just append new bits, doubled up (and possibly delayed).
// TODO: I can kill the conditional here. Probably?
if(source & high_resolution_mask_ & 0x80) {
ntsc_shift_ = ((doubled_source & 0x1fff) << 19) | ((ntsc_shift_ >> 13) & 0x40000) | (ntsc_shift_ >> 14);
} else {
ntsc_shift_ = (doubled_source << 18) | (ntsc_shift_ >> 14);
}
target = output_shift(target, 1 + c*2);
}
return target;
}
uint16_t *Video::output_double_high_resolution(uint16_t *target, int start, int end, int row) {
const uint16_t row_address = get_row_address(row);
for(int c = start; c < end; c++) {
const uint8_t source[2] = {
ram_[0x10000 + row_address + c],
ram_[row_address + c],
};
ntsc_shift_ = unsigned(source[1] << 25) | unsigned(source[0] << 18) | (ntsc_shift_ >> 14);
target = output_shift(target, c*2);
}
return target;
}
uint16_t *Video::output_shift(uint16_t *target, int column) {
// Make sure that at least two columns are enqueued before output begins;
// the top bits can't be understood without reference to bits that come afterwards.
if(!column) {
ntsc_shift_ |= ntsc_shift_ >> 14;
return target;
}
// Phase here is kind of arbitrary; it pairs off with the order
// I've picked for my rolls table and with my decision to count
// columns as aligned with double-mode.
const int phase = column * 7 + 3;
constexpr uint8_t rolls[4][16] = {
{
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf
},
{
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
},
{
0x0, 0x4, 0x8, 0xc, 0x1, 0x5, 0x9, 0xd,
0x2, 0x6, 0xa, 0xe, 0x3, 0x7, 0xb, 0xf
},
{
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
},
};
#define OutputPixel(offset) {\
ntsc_delay_ = ntsc_delay_lookup_[unsigned(ntsc_delay_ << 2) | ((ntsc_shift_ >> offset)&1) | ((ntsc_shift_ >> (offset + 3))&2)]; \
const auto raw_bits = (ntsc_shift_ >> (offset + ntsc_delay_)) & 0x0f; \
target[offset] = appleii_palette[rolls[(phase + offset + ntsc_delay_)&3][raw_bits]]; \
}
OutputPixel(0);
OutputPixel(1);
OutputPixel(2);
OutputPixel(3);
OutputPixel(4);
OutputPixel(5);
OutputPixel(6);
OutputPixel(7);
OutputPixel(8);
OutputPixel(9);
OutputPixel(10);
OutputPixel(11);
OutputPixel(12);
OutputPixel(13);
#undef OutputPixel
return target + 14;
}

View File

@ -0,0 +1,213 @@
//
// Video.hpp
// Clock Signal
//
// Created by Thomas Harte on 31/10/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef Apple_IIgs_Video_hpp
#define Apple_IIgs_Video_hpp
#include "../AppleII/VideoSwitches.hpp"
#include "../../../Outputs/CRT/CRT.hpp"
#include "../../../ClockReceiver/ClockReceiver.hpp"
namespace Apple {
namespace IIgs {
namespace Video {
/*!
Provides IIgs video output; assumed clocking here is seven times the usual Apple II clock.
So it'll produce a single line of video every 456 cycles 65*7 + 1, allowing for the
stretched cycle.
*/
class Video: public Apple::II::VideoSwitches<Cycles> {
public:
Video();
void set_internal_ram(const uint8_t *);
bool get_is_vertical_blank(Cycles offset);
uint8_t get_horizontal_counter(Cycles offset);
uint8_t get_vertical_counter(Cycles offset);
void set_new_video(uint8_t);
uint8_t get_new_video();
void clear_interrupts(uint8_t);
uint8_t get_interrupt_register();
void set_interrupt_register(uint8_t);
bool get_interrupt_line();
void notify_clock_tick();
void set_border_colour(uint8_t);
void set_text_colour(uint8_t);
uint8_t get_text_colour();
uint8_t get_border_colour();
void set_composite_is_colour(bool);
bool get_composite_is_colour();
/// Sets the scan target.
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
/// Gets the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const;
/// Sets the type of output.
void set_display_type(Outputs::Display::DisplayType);
/// Gets the type of output.
Outputs::Display::DisplayType get_display_type() const;
/// Determines the period until video might autonomously update its interrupt lines.
Cycles get_next_sequence_point() const;
/// Sets the Mega II interrupt enable state — 1/4-second and VBL interrupts are
/// generated here.
void set_megaii_interrupts_enabled(uint8_t);
uint8_t get_megaii_interrupt_status();
void clear_megaii_interrupts();
private:
Outputs::CRT::CRT crt_;
// This is coupled to Apple::II::GraphicsMode, but adds detail for the IIgs.
enum class GraphicsMode {
Text = 0,
DoubleText,
HighRes,
DoubleHighRes,
LowRes,
DoubleLowRes,
FatLowRes,
// Additions:
DoubleHighResMono,
SuperHighRes
};
constexpr bool is_colour_ntsc(GraphicsMode m) { return m >= GraphicsMode::HighRes && m <= GraphicsMode::FatLowRes; }
GraphicsMode graphics_mode(int row) const {
if(new_video_ & 0x80) {
return GraphicsMode::SuperHighRes;
}
const auto ii_mode = Apple::II::VideoSwitches<Cycles>::graphics_mode(row);
switch(ii_mode) {
// Coupling very much assumed here.
case Apple::II::GraphicsMode::DoubleHighRes:
if(new_video_ & 0x20) {
return GraphicsMode::DoubleHighResMono;
}
[[fallthrough]];
default: return GraphicsMode(int(ii_mode)); break;
}
}
enum class PixelBufferFormat {
Text, DoubleText, NTSC, NTSCMono, SuperHighRes
};
constexpr PixelBufferFormat format_for_mode(GraphicsMode m) {
switch(m) {
case GraphicsMode::Text: return PixelBufferFormat::Text;
case GraphicsMode::DoubleText: return PixelBufferFormat::DoubleText;
default: return PixelBufferFormat::NTSC;
case GraphicsMode::DoubleHighResMono: return PixelBufferFormat::NTSCMono;
case GraphicsMode::SuperHighRes: return PixelBufferFormat::SuperHighRes;
}
}
void advance(Cycles);
uint8_t new_video_ = 0x01;
uint8_t interrupts_ = 0x00;
void set_interrupts(uint8_t);
int cycles_into_frame_ = 0;
const uint8_t *ram_ = nullptr;
// The modal colours.
uint16_t border_colour_ = 0;
uint8_t border_colour_entry_ = 0;
uint8_t text_colour_entry_ = 0xf0;
uint16_t text_colour_ = 0xffff;
uint16_t background_colour_ = 0;
// Current pixel output buffer and conceptual format.
PixelBufferFormat pixels_format_;
uint16_t *pixels_ = nullptr, *next_pixel_ = nullptr;
int pixels_start_column_;
void output_row(int row, int start, int end);
uint16_t *output_super_high_res(uint16_t *target, int start, int end, int row) const;
uint16_t *output_text(uint16_t *target, int start, int end, int row) const;
uint16_t *output_double_text(uint16_t *target, int start, int end, int row) const;
uint16_t *output_char(uint16_t *target, uint8_t source, int row) const;
uint16_t *output_low_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_fat_low_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_double_low_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_high_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_double_high_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_double_high_resolution_mono(uint16_t *target, int start, int end, int row);
// Super high-res per-line state.
uint8_t line_control_;
uint16_t palette_[16];
// Storage used for fill mode.
uint16_t *palette_zero_[4] = {nullptr, nullptr, nullptr, nullptr}, palette_throwaway_;
// Lookup tables and state to assist in the IIgs' mapping from NTSC to RGB.
//
// My understanding of the real-life algorithm is: maintain a four-bit buffer.
// Fill it in a circular fashion. Ordinarily, output the result of looking
// up the RGB mapping of those four bits of Apple II output (which outputs four
// bits per NTSC colour cycle), commuted as per current phase. But if the bit
// being inserted differs from that currently in its position in the shift
// register, hold the existing output for three shifts.
//
// From there I am using the following:
// Maps from:
//
// b0 = b0 of the shift register
// b1 = b4 of the shift register
// b2 = current delay count
//
// to a new delay count.
uint8_t ntsc_delay_lookup_[20];
uint32_t ntsc_shift_ = 0; // Assumption here: logical shifts will ensue, rather than arithmetic.
int ntsc_delay_ = 0;
/// Outputs the lowest 14 bits from @c ntsc_shift_, mapping to RGB.
/// Phase is derived from @c column.
uint16_t *output_shift(uint16_t *target, int column);
// Common getter for the two counters.
struct Counters {
Counters(int v, int h) : vertical(v), horizontal(h) {}
const int vertical, horizontal;
};
Counters get_counters(Cycles offset);
// Marshalls the Mega II-style interrupt state.
uint8_t megaii_interrupt_mask_ = 0;
uint8_t megaii_interrupt_state_ = 0;
int megaii_frame_counter_ = 0; // To count up to quarter-second interrupts.
};
}
}
}
#endif /* Video_hpp */

View File

@ -13,7 +13,6 @@
#include "DeferredAudio.hpp"
#include "DriveSpeedAccumulator.hpp"
#include "Keyboard.hpp"
#include "RealTimeClock.hpp"
#include "Video.hpp"
#include "../../MachineTypes.hpp"
@ -32,6 +31,7 @@
#include "../../../Components/5380/ncr5380.hpp"
#include "../../../Components/6522/6522.hpp"
#include "../../../Components/8530/z8530.hpp"
#include "../../../Components/AppleClock/AppleClock.hpp"
#include "../../../Components/DiskII/IWM.hpp"
#include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp"
#include "../../../Processors/68000/68000.hpp"
@ -648,7 +648,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
class VIAPortHandler: public MOS::MOS6522::PortHandler {
public:
VIAPortHandler(ConcreteMachine &machine, RealTimeClock &clock, Keyboard &keyboard, DeferredAudio &audio, IWMActor &iwm, Inputs::QuadratureMouse &mouse) :
VIAPortHandler(ConcreteMachine &machine, Apple::Clock::SerialClock &clock, Keyboard &keyboard, DeferredAudio &audio, IWMActor &iwm, Inputs::QuadratureMouse &mouse) :
machine_(machine), clock_(clock), keyboard_(keyboard), audio_(audio), iwm_(iwm), mouse_(mouse) {}
using Port = MOS::MOS6522::Port;
@ -751,7 +751,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
private:
ConcreteMachine &machine_;
RealTimeClock &clock_;
Apple::Clock::SerialClock &clock_;
Keyboard &keyboard_;
DeferredAudio &audio_;
IWMActor &iwm_;
@ -766,7 +766,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
DeferredAudio audio_;
Video video_;
RealTimeClock clock_;
Apple::Clock::SerialClock clock_;
Keyboard keyboard_;
MOS::MOS6522::MOS6522<VIAPortHandler> via_;

View File

@ -1,173 +0,0 @@
//
// RealTimeClock.hpp
// Clock Signal
//
// Created by Thomas Harte on 07/05/2019.
// Copyright © 2019 Thomas Harte. All rights reserved.
//
#ifndef RealTimeClock_hpp
#define RealTimeClock_hpp
#include "../../Utility/MemoryFuzzer.hpp"
namespace Apple {
namespace Macintosh {
/*!
Models the storage component of Apple's real-time clock.
Since tracking of time is pushed to this class, it is assumed
that whomever is translating real time into emulated time
will notify the VIA of a potential interrupt.
*/
class RealTimeClock {
public:
RealTimeClock() {
// TODO: this should persist, if possible, rather than
// being default initialised.
const uint8_t default_data[] = {
0xa8, 0x00, 0x00, 0x00,
0xcc, 0x0a, 0xcc, 0x0a,
0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0x63, 0x00,
0x03, 0x88, 0x00, 0x4c
};
memcpy(data_, default_data, sizeof(data_));
}
/*!
Advances the clock by 1 second.
The caller should also notify the VIA.
*/
void update() {
for(int c = 0; c < 4; ++c) {
++seconds_[c];
if(seconds_[c]) break;
}
}
/*!
Sets the current clock and data inputs to the clock.
*/
void set_input(bool clock, bool data) {
/*
Documented commands:
z0000001 Seconds register 0 (lowest order byte)
z0000101 Seconds register 1
z0001001 Seconds register 2
z0001101 Seconds register 3
00110001 Test register (write only)
00110101 Write-protect register (write only)
z010aa01 RAM addresses 0x10 - 0x13
z1aaaa01 RAM addresses 0x00 0x0f
z = 1 => a read; z = 0 => a write.
The top bit of the write-protect register enables (0) or disables (1)
writes to other locations.
All the documentation says about the test register is to set the top
two bits to 0 for normal operation. Abnormal operation is undefined.
The data line is valid when the clock transitions to level 0.
*/
if(clock && !previous_clock_) {
// Shift into the command_ register, no matter what.
command_ = uint16_t((command_ << 1) | (data ? 1 : 0));
result_ <<= 1;
// Increment phase.
++phase_;
// When phase hits 8, inspect the command.
// If it's a read, prepare a result.
if(phase_ == 8) {
if(command_ & 0x80) {
// A read.
const auto address = (command_ >> 2) & 0x1f;
// Begin pessimistically.
result_ = 0xff;
if(address < 4) {
result_ = seconds_[address];
} else if(address >= 0x10) {
result_ = data_[address & 0xf];
} else if(address >= 0x8 && address <= 0xb) {
result_ = data_[0x10 + (address & 0x3)];
}
}
}
// If phase hits 16 and this was a read command,
// just stop. If it was a write command, do the
// actual write.
if(phase_ == 16) {
if(!(command_ & 0x8000)) {
// A write.
const auto address = (command_ >> 10) & 0x1f;
const uint8_t value = uint8_t(command_ & 0xff);
// First test: is this to the write-protect register?
if(address == 0xd) {
write_protect_ = value;
}
// No other writing is permitted if the write protect
// register won't allow it.
if(!(write_protect_ & 0x80)) {
if(address < 4) {
seconds_[address] = value;
} else if(address >= 0x10) {
data_[address & 0xf] = value;
} else if(address >= 0x8 && address <= 0xb) {
data_[0x10 + (address & 0x3)] = value;
}
}
}
// A phase of 16 always ends the command, so reset here.
abort();
}
}
previous_clock_ = clock;
}
/*!
Reads the current data output level from the clock.
*/
bool get_data() {
return !!(result_ & 0x80);
}
/*!
Announces that a serial command has been aborted.
*/
void abort() {
result_ = 0;
phase_ = 0;
command_ = 0;
}
private:
uint8_t data_[0x14];
uint8_t seconds_[4];
uint8_t write_protect_;
int phase_ = 0;
uint16_t command_;
uint8_t result_ = 0;
bool previous_clock_ = false;
};
}
}
#endif /* RealTimeClock_hpp */

View File

@ -465,16 +465,14 @@ class ConcreteMachine:
}
// Update the video output, checking whether a sequence point has been hit.
while(length >= cycles_until_video_event_) {
length -= cycles_until_video_event_;
video_ += cycles_until_video_event_;
cycles_until_video_event_ = video_->get_next_sequence_point();
assert(cycles_until_video_event_ > HalfCycles(0));
if(video_.will_flush(length)) {
length -= video_.cycles_until_implicit_flush();
video_ += video_.cycles_until_implicit_flush();
mfp_->set_timer_event_input(1, video_->display_enabled());
update_interrupt_input();
}
cycles_until_video_event_ -= length;
video_ += length;
}
@ -486,7 +484,6 @@ class ConcreteMachine:
HalfCycles bus_phase_;
JustInTimeActor<Video> video_;
HalfCycles cycles_until_video_event_;
// The MFP runs at 819200/2673749ths of the CPU clock rate.
JustInTimeActor<Motorola::MFP68901::MFP68901, 819200, 2673749> mfp_;

View File

@ -763,10 +763,10 @@ Machine *Machine::Oric(const Analyser::Static::Target *target_hint, const ROMMac
#define DiskInterfaceSwitch(processor) \
switch(oric_target->disk_interface) { \
default: return new ConcreteMachine<DiskInterface::None, processor>(*oric_target, rom_fetcher); \
default: return new ConcreteMachine<DiskInterface::None, processor>(*oric_target, rom_fetcher); \
case DiskInterface::Microdisc: return new ConcreteMachine<DiskInterface::Microdisc, processor>(*oric_target, rom_fetcher); \
case DiskInterface::Pravetz: return new ConcreteMachine<DiskInterface::Pravetz, processor>(*oric_target, rom_fetcher); \
case DiskInterface::Jasmin: return new ConcreteMachine<DiskInterface::Jasmin, processor>(*oric_target, rom_fetcher); \
case DiskInterface::Pravetz: return new ConcreteMachine<DiskInterface::Pravetz, processor>(*oric_target, rom_fetcher); \
case DiskInterface::Jasmin: return new ConcreteMachine<DiskInterface::Jasmin, processor>(*oric_target, rom_fetcher); \
case DiskInterface::BD500: return new ConcreteMachine<DiskInterface::BD500, processor>(*oric_target, rom_fetcher); \
}
@ -777,6 +777,7 @@ Machine *Machine::Oric(const Analyser::Static::Target *target_hint, const ROMMac
#undef DiskInterfaceSwitch
return nullptr;
}
Machine::~Machine() {}

View File

@ -10,9 +10,10 @@
#include <algorithm>
// Sources for runtime options.
// Sources for runtime options and machines.
#include "../AmstradCPC/AmstradCPC.hpp"
#include "../Apple/AppleII/AppleII.hpp"
#include "../Apple/AppleIIgs/AppleIIgs.hpp"
#include "../Apple/Macintosh/Macintosh.hpp"
#include "../Atari/2600/Atari2600.hpp"
#include "../Atari/ST/AtariST.hpp"
@ -28,6 +29,7 @@
#include "../../Analyser/Static/Acorn/Target.hpp"
#include "../../Analyser/Static/AmstradCPC/Target.hpp"
#include "../../Analyser/Static/AppleII/Target.hpp"
#include "../../Analyser/Static/AppleIIgs/Target.hpp"
#include "../../Analyser/Static/Atari2600/Target.hpp"
#include "../../Analyser/Static/AtariST/Target.hpp"
#include "../../Analyser/Static/Commodore/Target.hpp"
@ -50,6 +52,7 @@ Machine::DynamicMachine *Machine::MachineForTarget(const Analyser::Static::Targe
switch(target->machine) {
Bind(AmstradCPC)
BindD(Apple::II, AppleII)
BindD(Apple::IIgs, AppleIIgs)
BindD(Apple::Macintosh, Macintosh)
Bind(Atari2600)
BindD(Atari::ST, AtariST)
@ -116,6 +119,7 @@ std::string Machine::ShortNameForTargetMachine(const Analyser::Machine machine)
switch(machine) {
case Analyser::Machine::AmstradCPC: return "AmstradCPC";
case Analyser::Machine::AppleII: return "AppleII";
case Analyser::Machine::AppleIIgs: return "AppleIIgs";
case Analyser::Machine::Atari2600: return "Atari2600";
case Analyser::Machine::AtariST: return "AtariST";
case Analyser::Machine::ColecoVision: return "ColecoVision";
@ -135,6 +139,7 @@ std::string Machine::LongNameForTargetMachine(Analyser::Machine machine) {
switch(machine) {
case Analyser::Machine::AmstradCPC: return "Amstrad CPC";
case Analyser::Machine::AppleII: return "Apple II";
case Analyser::Machine::AppleIIgs: return "Apple IIgs";
case Analyser::Machine::Atari2600: return "Atari 2600";
case Analyser::Machine::AtariST: return "Atari ST";
case Analyser::Machine::ColecoVision: return "ColecoVision";
@ -164,6 +169,7 @@ std::vector<std::string> Machine::AllMachines(Type type, bool long_names) {
if(type == Type::Any || type == Type::DoesntRequireMedia) {
AddName(AmstradCPC);
AddName(AppleII);
AddName(AppleIIgs);
AddName(AtariST);
AddName(Electron);
AddName(Macintosh);
@ -210,6 +216,7 @@ std::map<std::string, std::unique_ptr<Analyser::Static::Target>> Machine::Target
Add(AmstradCPC);
Add(AppleII);
Add(AppleIIgs);
Add(AtariST);
AddMapped(Electron, Acorn);
Add(Macintosh);

View File

@ -21,9 +21,9 @@ void Fuzz(uint8_t *buffer, std::size_t size);
/// Stores @c size random 16-bit words from @c buffer onwards.
void Fuzz(uint16_t *buffer, std::size_t size);
/// Replaces all existing vector contents with random bytes.
template <typename T> void Fuzz(std::vector<T> &buffer) {
Fuzz(reinterpret_cast<uint8_t *>(buffer.data()), buffer.size() * sizeof(buffer[0]));
/// Replaces all existing vector or array contents with random bytes.
template <typename T> void Fuzz(T &buffer) {
Fuzz(reinterpret_cast<uint8_t *>(buffer.data()), buffer.size() * sizeof(typename T::value_type));
}
}

View File

@ -466,7 +466,7 @@ template<bool is_zx81> class ConcreteMachine:
void set_use_fast_tape() {
use_fast_tape_hack_ = allow_fast_tape_hack_ && tape_player_.has_tape();
}
bool use_automatic_tape_motor_control_;
bool use_automatic_tape_motor_control_ = true;
HalfCycles tape_advance_delay_ = 0;
// MARK: - Video

File diff suppressed because it is too large Load Diff

View File

@ -62,7 +62,7 @@
</CommandLineArgument>
<CommandLineArgument
argument = "/Users/thomasharte/Library/Mobile\ Documents/com\~apple\~CloudDocs/Desktop/Soft/Apple\ II/Keplermatik.dsk"
isEnabled = "YES">
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "/Users/thomasharte/Library/Mobile\ Documents/com\~apple\~CloudDocs/Desktop/Soft/Apple\ II/WOZs/Prince\ of\ Persia\ side\ A.woz"
@ -106,11 +106,11 @@
</CommandLineArgument>
<CommandLineArgument
argument = "--rompath=/Users/thomasharte/Projects/CLK/ROMImages"
isEnabled = "YES">
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--help"
isEnabled = "NO">
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--model=cpc6128"

View File

@ -552,6 +552,26 @@
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>2mg</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>floppy35.png</string>
<key>CFBundleTypeName</key>
<string>Apple 2MG Disk Image</string>
<key>CFBundleTypeOSTypes</key>
<array>
<string>????</string>
</array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>

View File

@ -204,17 +204,19 @@ struct ActivityObserver: public Activity::Observer {
[_delegateMachineAccessLock lock];
_speakerDelegate.machine = nil;
[_delegateMachineAccessLock unlock];
}
// [_view performWithGLContext:^{
// @synchronized(self) {
// self->_scanTarget.reset();
// }
// }];
- (Outputs::Speaker::Speaker *)speaker {
const auto audio_producer = _machine->audio_producer();
if(!audio_producer) {
return nullptr;
}
return audio_producer->get_speaker();
}
- (float)idealSamplingRateFromRange:(NSRange)range {
@synchronized(self) {
Outputs::Speaker::Speaker *speaker = _machine->audio_producer()->get_speaker();
Outputs::Speaker::Speaker *speaker = [self speaker];
if(speaker) {
return speaker->get_ideal_clock_rate_in_range((float)range.location, (float)(range.location + range.length));
}
@ -224,7 +226,7 @@ struct ActivityObserver: public Activity::Observer {
- (BOOL)isStereo {
@synchronized(self) {
Outputs::Speaker::Speaker *speaker = _machine->audio_producer()->get_speaker();
Outputs::Speaker::Speaker *speaker = [self speaker];
if(speaker) {
return speaker->get_is_stereo();
}
@ -240,7 +242,7 @@ struct ActivityObserver: public Activity::Observer {
- (BOOL)setSpeakerDelegate:(Outputs::Speaker::Speaker::Delegate *)delegate sampleRate:(float)sampleRate bufferSize:(NSUInteger)bufferSize stereo:(BOOL)stereo {
@synchronized(self) {
Outputs::Speaker::Speaker *speaker = _machine->audio_producer()->get_speaker();
Outputs::Speaker::Speaker *speaker = [self speaker];
if(speaker) {
speaker->set_output_rate(sampleRate, (int)bufferSize, stereo);
speaker->set_delegate(delegate);
@ -638,7 +640,7 @@ struct ActivityObserver: public Activity::Observer {
- (void)setVolume:(float)volume {
@synchronized(self) {
Outputs::Speaker::Speaker *speaker = _machine->audio_producer()->get_speaker();
Outputs::Speaker::Speaker *speaker = [self speaker];
if(speaker) {
return speaker->set_output_volume(volume);
}
@ -647,7 +649,7 @@ struct ActivityObserver: public Activity::Observer {
- (BOOL)hasAudioOutput {
@synchronized(self) {
Outputs::Speaker::Speaker *speaker = _machine->audio_producer()->get_speaker();
Outputs::Speaker::Speaker *speaker = [self speaker];
return speaker ? YES : NO;
}
}
@ -672,8 +674,10 @@ struct ActivityObserver: public Activity::Observer {
@synchronized(self) {
// Store a means to map from CVTimeStamp.hostTime to Time::Nanos;
// there is an extremely dodgy assumption here that the former is in ns.
// If you can find a well-defined way to get the CVTimeStamp.hostTime units,
// whether at runtime or via preprocessor define, I'd love to know about it.
if(!_timeDiff) {
_timeDiff = int64_t(now->hostTime) - int64_t(timeNow);
_timeDiff = int64_t(timeNow) - int64_t(now->hostTime);
}
// Store the next end-of-frame time. TODO: and start of next and implied visible duration, if raster racing?
@ -692,7 +696,7 @@ struct ActivityObserver: public Activity::Observer {
}
}
#define TICKS 600
#define TICKS 1000
- (void)start {
__block auto lastTime = Time::nanos_now();
@ -715,18 +719,29 @@ struct ActivityObserver: public Activity::Observer {
}
// If this tick includes vsync then inspect the machine.
if(timeNow >= self->_syncTime && lastTime < self->_syncTime) {
//
// _syncTime = 0 is used here as a sentinel to mark that a sync time is known;
// this with the >= test ensures that no syncs are missed even if some sort of
// performance problem is afoot (e.g. I'm debugging).
if(self->_syncTime && timeNow >= self->_syncTime) {
splitAndSync = self->_isSyncLocking = self->_scanSynchroniser.can_synchronise(self->_machine->scan_producer()->get_scan_status(), self->_refreshPeriod);
// If the time window is being split, run up to the split, then check out machine speed, possibly
// adjusting multiplier, then run after the split.
// adjusting multiplier, then run after the split. Include a sanity check against an out-of-bounds
// _syncTime; that can happen when debugging (possibly inter alia?).
if(splitAndSync) {
self->_machine->timed_machine()->run_for((double)(self->_syncTime - lastTime) / 1e9);
self->_machine->timed_machine()->set_speed_multiplier(
self->_scanSynchroniser.next_speed_multiplier(self->_machine->scan_producer()->get_scan_status())
);
self->_machine->timed_machine()->run_for((double)(timeNow - self->_syncTime) / 1e9);
if(self->_syncTime >= lastTime) {
self->_machine->timed_machine()->run_for((double)(self->_syncTime - lastTime) / 1e9);
self->_machine->timed_machine()->set_speed_multiplier(
self->_scanSynchroniser.next_speed_multiplier(self->_machine->scan_producer()->get_scan_status())
);
self->_machine->timed_machine()->run_for((double)(timeNow - self->_syncTime) / 1e9);
} else {
self->_machine->timed_machine()->run_for((double)(timeNow - lastTime) / 1e9);
}
}
self->_syncTime = 0;
}
// If the time window is being split, run up to the split, then check out machine speed, possibly

View File

@ -25,6 +25,12 @@ typedef NS_ENUM(NSInteger, CSMachineAppleIIDiskController) {
CSMachineAppleIIDiskControllerThirteenSector
};
typedef NS_ENUM(NSInteger, CSMachineAppleIIgsModel) {
CSMachineAppleIIgsModelROM00,
CSMachineAppleIIgsModelROM01,
CSMachineAppleIIgsModelROM03,
};
typedef NS_ENUM(NSInteger, CSMachineAtariSTModel) {
CSMachineAtariSTModel512k,
};
@ -76,16 +82,17 @@ typedef int Kilobytes;
- (nullable instancetype)initWithFileAtURL:(NSURL *)url;
- (instancetype)initWithElectronDFS:(BOOL)dfs adfs:(BOOL)adfs ap6:(BOOL)ap6 sidewaysRAM:(BOOL)sidewaysRAM;
- (instancetype)initWithAmstradCPCModel:(CSMachineCPCModel)model;
- (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model diskController:(CSMachineAppleIIDiskController)diskController;
- (instancetype)initWithAppleIIgsModel:(CSMachineAppleIIgsModel)model memorySize:(Kilobytes)memorySize;
- (instancetype)initWithAtariSTModel:(CSMachineAtariSTModel)model;
- (instancetype)initWithElectronDFS:(BOOL)dfs adfs:(BOOL)adfs ap6:(BOOL)ap6 sidewaysRAM:(BOOL)sidewaysRAM;
- (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model;
- (instancetype)initWithMSXRegion:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive;
- (instancetype)initWithOricModel:(CSMachineOricModel)model diskInterface:(CSMachineOricDiskInterface)diskInterface;
- (instancetype)initWithVic20Region:(CSMachineVic20Region)region memorySize:(Kilobytes)memorySize hasC1540:(BOOL)hasC1540;
- (instancetype)initWithZX80MemorySize:(Kilobytes)memorySize useZX81ROM:(BOOL)useZX81ROM;
- (instancetype)initWithZX81MemorySize:(Kilobytes)memorySize;
- (instancetype)initWithAppleIIModel:(CSMachineAppleIIModel)model diskController:(CSMachineAppleIIDiskController)diskController;
- (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model;
- (instancetype)initWithAtariSTModel:(CSMachineAtariSTModel)model;
@property(nonatomic, readonly, nullable) NSString *optionsPanelNibName;
@property(nonatomic, readonly) NSString *displayName;

View File

@ -16,6 +16,7 @@
#include "../../../../../Analyser/Static/Acorn/Target.hpp"
#include "../../../../../Analyser/Static/AmstradCPC/Target.hpp"
#include "../../../../../Analyser/Static/AppleII/Target.hpp"
#include "../../../../../Analyser/Static/AppleIIgs/Target.hpp"
#include "../../../../../Analyser/Static/AtariST/Target.hpp"
#include "../../../../../Analyser/Static/Commodore/Target.hpp"
#include "../../../../../Analyser/Static/Macintosh/Target.hpp"
@ -83,6 +84,27 @@
return self;
}
- (instancetype)initWithAppleIIgsModel:(CSMachineAppleIIgsModel)model memorySize:(Kilobytes)memorySize {
self = [super init];
if(self) {
using Target = Analyser::Static::AppleIIgs::Target;
auto target = std::make_unique<Target>();
switch(model) {
default: target->model = Target::Model::ROM00; break;
case CSMachineAppleIIgsModelROM01: target->model = Target::Model::ROM01; break;
case CSMachineAppleIIgsModelROM03: target->model = Target::Model::ROM03; break;
}
switch(memorySize) {
default: target->memory_model = Target::MemoryModel::EightMB; break;
case 1024: target->memory_model = Target::MemoryModel::OneMB; break;
case 256: target->memory_model = Target::MemoryModel::TwoHundredAndFiftySixKB; break;
}
_targets.push_back(std::move(target));
}
return self;
}
- (instancetype)initWithAtariSTModel:(CSMachineAtariSTModel)model {
self = [super init];
if(self) {

View File

@ -17,14 +17,14 @@
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" titleVisibility="hidden" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" documentModal="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="725" height="205"/>
<rect key="contentRect" x="196" y="240" width="810" height="205"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1440"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="725" height="205"/>
<rect key="frame" x="0.0" y="0.0" width="810" height="205"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hKn-1l-OSN">
<rect key="frame" x="631" y="13" width="81" height="32"/>
<rect key="frame" x="716" y="13" width="81" height="32"/>
<buttonCell key="cell" type="push" title="Choose" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="MnM-xo-4Qa">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@ -37,7 +37,7 @@ DQ
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JQy-Cj-AOK">
<rect key="frame" x="558" y="13" width="76" height="32"/>
<rect key="frame" x="643" y="13" width="76" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="sub-rB-Req">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@ -59,12 +59,12 @@ Gw
</textFieldCell>
</textField>
<tabView translatesAutoresizingMaskIntoConstraints="NO" id="VUb-QG-x7c">
<rect key="frame" x="13" y="50" width="699" height="141"/>
<rect key="frame" x="13" y="50" width="784" height="141"/>
<font key="font" metaFont="system"/>
<tabViewItems>
<tabViewItem label="Apple II" identifier="appleii" id="P59-QG-LOa">
<view key="view" id="dHz-Yv-GNq">
<rect key="frame" x="10" y="33" width="679" height="95"/>
<rect key="frame" x="10" y="33" width="764" height="95"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="V5Z-dX-Ns4">
@ -128,9 +128,74 @@ Gw
</constraints>
</view>
</tabViewItem>
<tabViewItem label="Apple IIgs" identifier="appleiigs" id="u5E-8n-ghF">
<view key="view" id="jOM-9f-vkk">
<rect key="frame" x="10" y="33" width="764" height="95"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="0d9-IG-gKU">
<rect key="frame" x="15" y="74" width="46" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Model:" id="kiv-1P-FWc">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="LES-76-Ovz">
<rect key="frame" x="15" y="44" width="85" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Memory size:" id="OLJ-nC-yyj">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gcS-uy-mzl">
<rect key="frame" x="64" y="68" width="89" height="25"/>
<popUpButtonCell key="cell" type="push" title="ROM 03" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="2" imageScaling="proportionallyDown" inset="2" selectedItem="0TS-DO-O9h" id="hjw-g8-e2f">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="4rn-K4-QgV">
<items>
<menuItem title="ROM 00" id="2LO-US-byb"/>
<menuItem title="ROM 01" tag="1" id="oEx-uN-LlH"/>
<menuItem title="ROM 03" state="on" tag="2" id="0TS-DO-O9h"/>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nQa-YS-utT">
<rect key="frame" x="103" y="38" width="82" height="25"/>
<popUpButtonCell key="cell" type="push" title="8 mb" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="8192" imageScaling="proportionallyDown" inset="2" selectedItem="UHg-gU-Xnn" id="dl3-cq-uWO">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="Scm-d9-EU9">
<items>
<menuItem title="256 kb" tag="256" id="PZE-8h-MNs"/>
<menuItem title="1 mb" tag="1024" id="xas-28-obv"/>
<menuItem title="8 mb" state="on" tag="8192" id="UHg-gU-Xnn"/>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
</subviews>
<constraints>
<constraint firstItem="LES-76-Ovz" firstAttribute="leading" secondItem="jOM-9f-vkk" secondAttribute="leading" constant="17" id="2D3-Ve-6CN"/>
<constraint firstItem="0d9-IG-gKU" firstAttribute="leading" secondItem="jOM-9f-vkk" secondAttribute="leading" constant="17" id="8Xz-86-tDf"/>
<constraint firstItem="0d9-IG-gKU" firstAttribute="centerY" secondItem="gcS-uy-mzl" secondAttribute="centerY" id="Eww-Qq-eBT"/>
<constraint firstItem="gcS-uy-mzl" firstAttribute="top" secondItem="jOM-9f-vkk" secondAttribute="top" constant="3" id="F6i-cP-7AR"/>
<constraint firstItem="gcS-uy-mzl" firstAttribute="leading" secondItem="0d9-IG-gKU" secondAttribute="trailing" constant="8" id="LUm-rI-LYP"/>
<constraint firstItem="LES-76-Ovz" firstAttribute="centerY" secondItem="nQa-YS-utT" secondAttribute="centerY" id="UdP-U6-OFE"/>
<constraint firstItem="nQa-YS-utT" firstAttribute="leading" secondItem="LES-76-Ovz" secondAttribute="trailing" constant="8" id="Xm1-iG-Dgj"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="nQa-YS-utT" secondAttribute="trailing" constant="17" id="ZQ5-TG-yv1"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="gcS-uy-mzl" secondAttribute="trailing" constant="17" id="a38-Cx-CEh"/>
<constraint firstItem="nQa-YS-utT" firstAttribute="top" secondItem="gcS-uy-mzl" secondAttribute="bottom" constant="10" id="aM9-m8-s7z"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="nQa-YS-utT" secondAttribute="bottom" constant="3" id="sbT-If-NTU"/>
</constraints>
</view>
</tabViewItem>
<tabViewItem label="Amstrad CPC" identifier="cpc" id="JmB-OF-xcM">
<view key="view" id="5zS-Nj-Ynx">
<rect key="frame" x="10" y="33" width="679" height="95"/>
<rect key="frame" x="10" y="33" width="770" height="95"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="00d-sg-Krh">
@ -189,7 +254,7 @@ Gw
</tabViewItem>
<tabViewItem label="Electron" identifier="electron" id="muc-z9-Vqc">
<view key="view" id="SRc-2D-95G">
<rect key="frame" x="10" y="33" width="679" height="95"/>
<rect key="frame" x="10" y="33" width="764" height="95"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JqM-IK-FMP">
@ -325,11 +390,11 @@ Gw
</tabViewItem>
<tabViewItem label="Oric" identifier="oric" id="NSx-DC-p4M">
<view key="view" id="sOR-e0-8iZ">
<rect key="frame" x="10" y="33" width="674" height="94"/>
<rect key="frame" x="10" y="33" width="764" height="95"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="0ct-tf-uRH">
<rect key="frame" x="15" y="73" width="46" height="16"/>
<rect key="frame" x="15" y="74" width="46" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Model:" id="Xm1-7x-YVl">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -337,7 +402,7 @@ Gw
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ENP-hI-BVZ">
<rect key="frame" x="65" y="67" width="106" height="25"/>
<rect key="frame" x="64" y="68" width="107" height="25"/>
<popUpButtonCell key="cell" type="push" title="Oric-1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="jGN-1a-biF" id="Jll-EJ-cMr">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@ -351,7 +416,7 @@ Gw
</popUpButtonCell>
</popUpButton>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fYL-p6-wyn">
<rect key="frame" x="111" y="36" width="129" height="25"/>
<rect key="frame" x="110" y="38" width="130" height="25"/>
<popUpButtonCell key="cell" type="push" title="None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="XhK-Jh-oTW" id="aYb-m1-H9X">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@ -367,7 +432,7 @@ Gw
</popUpButtonCell>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="okM-ZI-NbF">
<rect key="frame" x="15" y="42" width="92" height="16"/>
<rect key="frame" x="15" y="44" width="92" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Disk interface:" id="SFK-hS-tFC">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -392,7 +457,7 @@ Gw
</tabViewItem>
<tabViewItem label="Vic-20" identifier="vic20" id="cyO-PU-hSU">
<view key="view" id="fLI-XB-QCr">
<rect key="frame" x="10" y="33" width="674" height="94"/>
<rect key="frame" x="10" y="33" width="680" height="94"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ueK-gq-gaF">
@ -517,7 +582,7 @@ Gw
</tabViewItem>
<tabViewItem label="ZX81" identifier="zx81" id="Wnn-nQ-gZ6">
<view key="view" id="bmd-gL-gzT">
<rect key="frame" x="10" y="33" width="604" height="94"/>
<rect key="frame" x="10" y="33" width="674" height="94"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5aO-UX-HnX">
@ -571,12 +636,14 @@ Gw
<constraint firstItem="VUb-QG-x7c" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" constant="20" id="zT3-Ea-QQJ"/>
</constraints>
</view>
<point key="canvasLocation" x="33.5" y="88.5"/>
<point key="canvasLocation" x="94.5" y="90.5"/>
</window>
<customObject id="192-Eb-Rpg" customClass="MachinePicker" customModule="Clock_Signal" customModuleProvider="target">
<connections>
<outlet property="appleIIDiskControllerButton" destination="LSB-WP-FMi" id="Ssa-jd-t63"/>
<outlet property="appleIIModelButton" destination="jli-ac-Sij" id="Jm3-f7-C17"/>
<outlet property="appleIIgsMemorySizeButton" destination="nQa-YS-utT" id="pTV-XL-zX3"/>
<outlet property="appleIIgsModelButton" destination="gcS-uy-mzl" id="Jcc-jC-cV1"/>
<outlet property="cpcModelTypeButton" destination="00d-sg-Krh" id="VyV-b1-A6x"/>
<outlet property="electronADFSButton" destination="945-wU-JOH" id="Fjm-W8-kvh"/>
<outlet property="electronAP6Button" destination="cG2-Ph-S3Z" id="vkq-1J-KBG"/>

View File

@ -9,43 +9,48 @@
import Cocoa
class MachinePicker: NSObject {
@IBOutlet var machineSelector: NSTabView?
@IBOutlet var machineSelector: NSTabView!
@IBOutlet var machineSelectionTabs: NSTabView!
// MARK: - Apple II properties
@IBOutlet var appleIIModelButton: NSPopUpButton?
@IBOutlet var appleIIDiskControllerButton: NSPopUpButton?
@IBOutlet var appleIIModelButton: NSPopUpButton!
@IBOutlet var appleIIDiskControllerButton: NSPopUpButton!
// MARK: - Apple IIgs properties
@IBOutlet var appleIIgsModelButton: NSPopUpButton!
@IBOutlet var appleIIgsMemorySizeButton: NSPopUpButton!
// MARK: - Electron properties
@IBOutlet var electronDFSButton: NSButton?
@IBOutlet var electronADFSButton: NSButton?
@IBOutlet var electronAP6Button: NSButton?
@IBOutlet var electronSidewaysRAMButton: NSButton?
@IBOutlet var electronDFSButton: NSButton!
@IBOutlet var electronADFSButton: NSButton!
@IBOutlet var electronAP6Button: NSButton!
@IBOutlet var electronSidewaysRAMButton: NSButton!
// MARK: - CPC properties
@IBOutlet var cpcModelTypeButton: NSPopUpButton?
@IBOutlet var cpcModelTypeButton: NSPopUpButton!
// MARK: - Macintosh properties
@IBOutlet var macintoshModelTypeButton: NSPopUpButton?
@IBOutlet var macintoshModelTypeButton: NSPopUpButton!
// MARK: - MSX properties
@IBOutlet var msxRegionButton: NSPopUpButton?
@IBOutlet var msxHasDiskDriveButton: NSButton?
@IBOutlet var msxRegionButton: NSPopUpButton!
@IBOutlet var msxHasDiskDriveButton: NSButton!
// MARK: - Oric properties
@IBOutlet var oricModelTypeButton: NSPopUpButton?
@IBOutlet var oricDiskInterfaceButton: NSPopUpButton?
@IBOutlet var oricModelTypeButton: NSPopUpButton!
@IBOutlet var oricDiskInterfaceButton: NSPopUpButton!
// MARK: - Vic-20 properties
@IBOutlet var vic20RegionButton: NSPopUpButton?
@IBOutlet var vic20MemorySizeButton: NSPopUpButton?
@IBOutlet var vic20HasC1540Button: NSButton?
@IBOutlet var vic20RegionButton: NSPopUpButton!
@IBOutlet var vic20MemorySizeButton: NSPopUpButton!
@IBOutlet var vic20HasC1540Button: NSButton!
// MARK: - ZX80 properties
@IBOutlet var zx80MemorySizeButton: NSPopUpButton?
@IBOutlet var zx80UsesZX81ROMButton: NSButton?
@IBOutlet var zx80MemorySizeButton: NSPopUpButton!
@IBOutlet var zx80UsesZX81ROMButton: NSButton!
// MARK: - ZX81 properties
@IBOutlet var zx81MemorySizeButton: NSPopUpButton?
@IBOutlet var zx81MemorySizeButton: NSPopUpButton!
// MARK: - Preferences
func establishStoredOptions() {
@ -55,106 +60,119 @@ class MachinePicker: NSObject {
if let machineIdentifier = standardUserDefaults.string(forKey: "new.machine") {
// If I've changed my mind about visible tabs between versions, there may not be one that corresponds
// to the stored identifier. Make sure not to raise an NSRangeException in that scenario.
if let index = machineSelector?.indexOfTabViewItem(withIdentifier: machineIdentifier as Any), index != NSNotFound {
machineSelector?.selectTabViewItem(at: index)
let index = machineSelector.indexOfTabViewItem(withIdentifier: machineIdentifier as Any)
if index != NSNotFound {
machineSelector.selectTabViewItem(at: index)
}
}
// Apple II settings
appleIIModelButton?.selectItem(withTag: standardUserDefaults.integer(forKey: "new.appleIIModel"))
appleIIDiskControllerButton?.selectItem(withTag: standardUserDefaults.integer(forKey: "new.appleIIDiskController"))
appleIIModelButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.appleIIModel"))
appleIIDiskControllerButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.appleIIDiskController"))
// Apple IIgs settings
appleIIgsModelButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.appleIIgsModel"))
appleIIgsMemorySizeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.appleIIgsMemorySize"))
// Electron settings
electronDFSButton?.state = standardUserDefaults.bool(forKey: "new.electronDFS") ? .on : .off
electronADFSButton?.state = standardUserDefaults.bool(forKey: "new.electronADFS") ? .on : .off
electronAP6Button?.state = standardUserDefaults.bool(forKey: "new.electronAP6") ? .on : .off
electronSidewaysRAMButton?.state = standardUserDefaults.bool(forKey: "new.electronSidewaysRAM") ? .on : .off
electronDFSButton.state = standardUserDefaults.bool(forKey: "new.electronDFS") ? .on : .off
electronADFSButton.state = standardUserDefaults.bool(forKey: "new.electronADFS") ? .on : .off
electronAP6Button.state = standardUserDefaults.bool(forKey: "new.electronAP6") ? .on : .off
electronSidewaysRAMButton.state = standardUserDefaults.bool(forKey: "new.electronSidewaysRAM") ? .on : .off
// CPC settings
cpcModelTypeButton?.selectItem(withTag: standardUserDefaults.integer(forKey: "new.cpcModel"))
cpcModelTypeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.cpcModel"))
// Macintosh settings
macintoshModelTypeButton?.selectItem(withTag: standardUserDefaults.integer(forKey: "new.macintoshModel"))
macintoshModelTypeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.macintoshModel"))
// MSX settings
msxRegionButton?.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxRegion"))
msxHasDiskDriveButton?.state = standardUserDefaults.bool(forKey: "new.msxDiskDrive") ? .on : .off
msxRegionButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxRegion"))
msxHasDiskDriveButton.state = standardUserDefaults.bool(forKey: "new.msxDiskDrive") ? .on : .off
// Oric settings
oricDiskInterfaceButton?.selectItem(withTag: standardUserDefaults.integer(forKey: "new.oricDiskInterface"))
oricModelTypeButton?.selectItem(withTag: standardUserDefaults.integer(forKey: "new.oricModel"))
oricDiskInterfaceButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.oricDiskInterface"))
oricModelTypeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.oricModel"))
// Vic-20 settings
vic20RegionButton?.selectItem(withTag: standardUserDefaults.integer(forKey: "new.vic20Region"))
vic20MemorySizeButton?.selectItem(withTag: standardUserDefaults.integer(forKey: "new.vic20MemorySize"))
vic20HasC1540Button?.state = standardUserDefaults.bool(forKey: "new.vic20C1540") ? .on : .off
vic20RegionButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.vic20Region"))
vic20MemorySizeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.vic20MemorySize"))
vic20HasC1540Button.state = standardUserDefaults.bool(forKey: "new.vic20C1540") ? .on : .off
// ZX80
zx80MemorySizeButton?.selectItem(withTag: standardUserDefaults.integer(forKey: "new.zx80MemorySize"))
zx80UsesZX81ROMButton?.state = standardUserDefaults.bool(forKey: "new.zx80UsesZX81ROM") ? .on : .off
zx80MemorySizeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.zx80MemorySize"))
zx80UsesZX81ROMButton.state = standardUserDefaults.bool(forKey: "new.zx80UsesZX81ROM") ? .on : .off
// ZX81
zx81MemorySizeButton?.selectItem(withTag: standardUserDefaults.integer(forKey: "new.zx81MemorySize"))
zx81MemorySizeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.zx81MemorySize"))
// TEMPORARY: remove the Apple IIgs option. It's not yet a fully-working machine; no need to publicise it.
let appleIIgsTabIndex = machineSelectionTabs.indexOfTabViewItem(withIdentifier: "appleiigs")
machineSelectionTabs.removeTabViewItem(machineSelectionTabs.tabViewItem(at: appleIIgsTabIndex))
}
fileprivate func storeOptions() {
let standardUserDefaults = UserDefaults.standard
// Machine type
standardUserDefaults.set(machineSelector!.selectedTabViewItem!.identifier as! String, forKey: "new.machine")
standardUserDefaults.set(machineSelector.selectedTabViewItem!.identifier as! String, forKey: "new.machine")
// Apple II settings
standardUserDefaults.set(appleIIModelButton!.selectedTag(), forKey: "new.appleIIModel")
standardUserDefaults.set(appleIIDiskControllerButton!.selectedTag(), forKey: "new.appleIIDiskController")
standardUserDefaults.set(appleIIModelButton.selectedTag(), forKey: "new.appleIIModel")
standardUserDefaults.set(appleIIDiskControllerButton.selectedTag(), forKey: "new.appleIIDiskController")
// Apple IIgs settings
standardUserDefaults.set(appleIIgsModelButton.selectedTag(), forKey: "new.appleIIgsModel")
standardUserDefaults.set(appleIIgsMemorySizeButton.selectedTag(), forKey: "new.appleIIgsMemorySize")
// Electron settings
standardUserDefaults.set(electronDFSButton!.state == .on, forKey: "new.electronDFS")
standardUserDefaults.set(electronADFSButton!.state == .on, forKey: "new.electronADFS")
standardUserDefaults.set(electronAP6Button!.state == .on, forKey: "new.electronAP6")
standardUserDefaults.set(electronSidewaysRAMButton!.state == .on, forKey: "new.electronSidewaysRAM")
standardUserDefaults.set(electronDFSButton.state == .on, forKey: "new.electronDFS")
standardUserDefaults.set(electronADFSButton.state == .on, forKey: "new.electronADFS")
standardUserDefaults.set(electronAP6Button.state == .on, forKey: "new.electronAP6")
standardUserDefaults.set(electronSidewaysRAMButton.state == .on, forKey: "new.electronSidewaysRAM")
// CPC settings
standardUserDefaults.set(cpcModelTypeButton!.selectedTag(), forKey: "new.cpcModel")
standardUserDefaults.set(cpcModelTypeButton.selectedTag(), forKey: "new.cpcModel")
// Macintosh settings
standardUserDefaults.set(macintoshModelTypeButton!.selectedTag(), forKey: "new.macintoshModel")
standardUserDefaults.set(macintoshModelTypeButton.selectedTag(), forKey: "new.macintoshModel")
// MSX settings
standardUserDefaults.set(msxRegionButton!.selectedTag(), forKey: "new.msxRegion")
standardUserDefaults.set(msxHasDiskDriveButton?.state == .on, forKey: "new.msxDiskDrive")
standardUserDefaults.set(msxRegionButton.selectedTag(), forKey: "new.msxRegion")
standardUserDefaults.set(msxHasDiskDriveButton.state == .on, forKey: "new.msxDiskDrive")
// Oric settings
standardUserDefaults.set(oricDiskInterfaceButton!.selectedTag(), forKey: "new.oricDiskInterface")
standardUserDefaults.set(oricModelTypeButton!.selectedTag(), forKey: "new.oricModel")
standardUserDefaults.set(oricDiskInterfaceButton.selectedTag(), forKey: "new.oricDiskInterface")
standardUserDefaults.set(oricModelTypeButton.selectedTag(), forKey: "new.oricModel")
// Vic-20 settings
standardUserDefaults.set(vic20RegionButton!.selectedTag(), forKey: "new.vic20Region")
standardUserDefaults.set(vic20MemorySizeButton!.selectedTag(), forKey: "new.vic20MemorySize")
standardUserDefaults.set(vic20HasC1540Button?.state == .on, forKey: "new.vic20C1540")
standardUserDefaults.set(vic20RegionButton.selectedTag(), forKey: "new.vic20Region")
standardUserDefaults.set(vic20MemorySizeButton.selectedTag(), forKey: "new.vic20MemorySize")
standardUserDefaults.set(vic20HasC1540Button.state == .on, forKey: "new.vic20C1540")
// ZX80
standardUserDefaults.set(zx80MemorySizeButton!.selectedTag(), forKey: "new.zx80MemorySize")
standardUserDefaults.set(zx80UsesZX81ROMButton?.state == .on, forKey: "new.zx80UsesZX81ROM")
standardUserDefaults.set(zx80MemorySizeButton.selectedTag(), forKey: "new.zx80MemorySize")
standardUserDefaults.set(zx80UsesZX81ROMButton.state == .on, forKey: "new.zx80UsesZX81ROM")
// ZX81
standardUserDefaults.set(zx81MemorySizeButton!.selectedTag(), forKey: "new.zx81MemorySize")
standardUserDefaults.set(zx81MemorySizeButton.selectedTag(), forKey: "new.zx81MemorySize")
}
// MARK: - Machine builder
func selectedMachine() -> CSStaticAnalyser {
storeOptions()
switch machineSelector!.selectedTabViewItem!.identifier as! String {
switch machineSelector.selectedTabViewItem!.identifier as! String {
case "electron":
return CSStaticAnalyser(
electronDFS: electronDFSButton!.state == .on,
adfs: electronADFSButton!.state == .on,
ap6: electronAP6Button!.state == .on,
sidewaysRAM: electronSidewaysRAMButton!.state == .on)
electronDFS: electronDFSButton.state == .on,
adfs: electronADFSButton.state == .on,
ap6: electronAP6Button.state == .on,
sidewaysRAM: electronSidewaysRAMButton.state == .on)
case "appleii":
var model: CSMachineAppleIIModel = .appleII
switch appleIIModelButton!.selectedTag() {
switch appleIIModelButton.selectedTag() {
case 1: model = .appleIIPlus
case 2: model = .appleIIe
case 3: model = .appleEnhancedIIe
@ -163,7 +181,7 @@ class MachinePicker: NSObject {
}
var diskController: CSMachineAppleIIDiskController = .none
switch appleIIDiskControllerButton!.selectedTag() {
switch appleIIDiskControllerButton.selectedTag() {
case 13: diskController = .thirteenSector
case 16: diskController = .sixteenSector
case 0: fallthrough
@ -172,11 +190,23 @@ class MachinePicker: NSObject {
return CSStaticAnalyser(appleIIModel: model, diskController: diskController)
case "appleiigs":
var model: CSMachineAppleIIgsModel = .ROM00
switch appleIIgsModelButton.selectedTag() {
case 1: model = .ROM01
case 2: model = .ROM03
case 0: fallthrough
default: model = .ROM00
}
let memorySize = Kilobytes(appleIIgsMemorySizeButton.selectedItem!.tag)
return CSStaticAnalyser(appleIIgsModel: model, memorySize: memorySize)
case "atarist":
return CSStaticAnalyser(atariSTModel: .model512k)
case "cpc":
switch cpcModelTypeButton!.selectedItem!.tag {
switch cpcModelTypeButton.selectedItem!.tag {
case 464: return CSStaticAnalyser(amstradCPCModel: .model464)
case 664: return CSStaticAnalyser(amstradCPCModel: .model664)
case 6128: fallthrough
@ -184,7 +214,7 @@ class MachinePicker: NSObject {
}
case "mac":
switch macintoshModelTypeButton!.selectedItem!.tag {
switch macintoshModelTypeButton.selectedItem!.tag {
case 0: return CSStaticAnalyser(macintoshModel: .model128k)
case 1: return CSStaticAnalyser(macintoshModel: .model512k)
case 2: return CSStaticAnalyser(macintoshModel: .model512ke)
@ -193,8 +223,8 @@ class MachinePicker: NSObject {
}
case "msx":
let hasDiskDrive = msxHasDiskDriveButton!.state == .on
switch msxRegionButton!.selectedItem?.tag {
let hasDiskDrive = msxHasDiskDriveButton.state == .on
switch msxRegionButton.selectedItem!.tag {
case 2:
return CSStaticAnalyser(msxRegion: .japanese, hasDiskDrive: hasDiskDrive)
case 1:
@ -206,7 +236,7 @@ class MachinePicker: NSObject {
case "oric":
var diskInterface: CSMachineOricDiskInterface = .none
switch oricDiskInterfaceButton!.selectedTag() {
switch oricDiskInterfaceButton.selectedTag() {
case 1: diskInterface = .microdisc
case 2: diskInterface = .pravetz
case 3: diskInterface = .jasmin
@ -215,7 +245,7 @@ class MachinePicker: NSObject {
}
var model: CSMachineOricModel = .oric1
switch oricModelTypeButton!.selectedItem!.tag {
switch oricModelTypeButton.selectedItem!.tag {
case 1: model = .oricAtmos
case 2: model = .pravetz
default: break;
@ -224,9 +254,9 @@ class MachinePicker: NSObject {
return CSStaticAnalyser(oricModel: model, diskInterface: diskInterface)
case "vic20":
let memorySize = Kilobytes(vic20MemorySizeButton!.selectedItem!.tag)
let hasC1540 = vic20HasC1540Button!.state == .on
switch vic20RegionButton!.selectedItem?.tag {
let memorySize = Kilobytes(vic20MemorySizeButton.selectedItem!.tag)
let hasC1540 = vic20HasC1540Button.state == .on
switch vic20RegionButton.selectedItem!.tag {
case 1:
return CSStaticAnalyser(vic20Region: .american, memorySize: memorySize, hasC1540: hasC1540)
case 2:
@ -241,10 +271,10 @@ class MachinePicker: NSObject {
}
case "zx80":
return CSStaticAnalyser(zx80MemorySize: Kilobytes(zx80MemorySizeButton!.selectedItem!.tag), useZX81ROM: zx80UsesZX81ROMButton!.state == .on)
return CSStaticAnalyser(zx80MemorySize: Kilobytes(zx80MemorySizeButton.selectedItem!.tag), useZX81ROM: zx80UsesZX81ROMButton.state == .on)
case "zx81":
return CSStaticAnalyser(zx81MemorySize: Kilobytes(zx81MemorySizeButton!.selectedItem!.tag))
return CSStaticAnalyser(zx81MemorySize: Kilobytes(zx81MemorySizeButton.selectedItem!.tag))
default: return CSStaticAnalyser()
}

View File

@ -410,7 +410,7 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
width:frameBufferWidth
height:frameBufferHeight
mipmapped:NO];
textureDescriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
textureDescriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite;
textureDescriptor.resourceOptions = MTLResourceStorageModePrivate;
id<MTLTexture> _oldFrameBuffer = _frameBuffer;
_frameBuffer = [_view.device newTextureWithDescriptor:textureDescriptor];
@ -443,19 +443,39 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
depthStencilDescriptor.frontFaceStencil.depthStencilPassOperation = MTLStencilOperationReplace;
_drawStencilState = [_view.device newDepthStencilStateWithDescriptor:depthStencilDescriptor];
// Draw from _oldFrameBuffer to _frameBuffer.
// Draw from _oldFrameBuffer to _frameBuffer; otherwise clear the new framebuffer.
if(_oldFrameBuffer) {
[self copyTexture:_oldFrameBuffer to:_frameBuffer];
// Don't clear the framebuffer at the end of this frame.
_dontClearFrameBuffer = YES;
} else {
// TODO: this use of clearTexture is the only reasn _frameBuffer has a marked usage of MTLTextureUsageShaderWrite;
// it'd probably be smarter to blank it with geometry rather than potentially complicating
// its storage further?
[self clearTexture:_frameBuffer];
}
// Don't clear the framebuffer at the end of this frame.
_dontClearFrameBuffer = YES;
}
- (BOOL)shouldApplyGamma {
return fabsf(float(uniforms()->outputGamma) - 1.0f) > 0.01f;
}
- (void)clearTexture:(id<MTLTexture>)texture {
id<MTLLibrary> library = [_view.device newDefaultLibrary];
// Ensure finalised line texture is initially clear.
id<MTLComputePipelineState> clearPipeline = [_view.device newComputePipelineStateWithFunction:[library newFunctionWithName:@"clearKernel"] error:nil];
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
id<MTLComputeCommandEncoder> computeEncoder = [commandBuffer computeCommandEncoder];
[computeEncoder setTexture:texture atIndex:0];
[self dispatchComputeCommandEncoder:computeEncoder pipelineState:clearPipeline width:texture.width height:texture.height offsetBuffer:[self bufferForOffset:0]];
[computeEncoder endEncoding];
[commandBuffer commit];
}
- (void)updateModalBuffers {
// Build a descriptor for any intermediate line texture.
MTLTextureDescriptor *const lineTextureDescriptor = [MTLTextureDescriptor
@ -490,19 +510,10 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
// The finalised texture will definitely exist, and may or may not require a gamma conversion when written to.
if(!_finalisedLineTexture) {
_finalisedLineTexture = [_view.device newTextureWithDescriptor:lineTextureDescriptor];
[self clearTexture:_finalisedLineTexture];
NSString *const kernelFunction = [self shouldApplyGamma] ? @"filterChromaKernelWithGamma" : @"filterChromaKernelNoGamma";
_finalisedLineState = [_view.device newComputePipelineStateWithFunction:[library newFunctionWithName:kernelFunction] error:nil];
// Ensure finalised line texture is initially clear.
id<MTLComputePipelineState> clearPipeline = [_view.device newComputePipelineStateWithFunction:[library newFunctionWithName:@"clearKernel"] error:nil];
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
id<MTLComputeCommandEncoder> computeEncoder = [commandBuffer computeCommandEncoder];
[self dispatchComputeCommandEncoder:computeEncoder pipelineState:clearPipeline width:lineTextureDescriptor.width height:lineTextureDescriptor.height offsetBuffer:[self bufferForOffset:0]];
[computeEncoder endEncoding];
[commandBuffer commit];
}
// A luma separation texture will exist only for composite colour.

View File

@ -582,7 +582,7 @@ kernel void separateLumaKernel5( texture2d<half, access::read> inTexture [[textu
return setSeparatedLumaChroma(luminance, centreSample, outTexture, gid, offset);
}
kernel void clearKernel( texture2d<half, access::write> outTexture [[texture(1)]],
kernel void clearKernel( texture2d<half, access::write> outTexture [[texture(0)]],
uint2 gid [[thread_position_in_grid]]) {
outTexture.write(half4(0.0f, 0.0f, 0.0f, 1.0f), gid);
}

View File

@ -0,0 +1,171 @@
//
// krom65816Tests.swift
// Clock Signal
//
// Created by Thomas Harte on 02/11/2020.
// Copyright 2020 Thomas Harte. All rights reserved.
//
import Foundation
import XCTest
// This utilises krom's SNES-centric 65816 tests, comparing step-by-step to
// the traces offered by LilaQ at emudev.de as I don't want to implement a
// SNES just for the sake of result inspection.
//
// So:
// https://github.com/PeterLemon/SNES/tree/master/CPUTest/CPU for the tests;
// https://emudev.de/q00-snes/65816-the-cpu/ for the traces.
class Krom65816Tests: XCTestCase {
// MARK: - Test Runner
func runTest(_ name: String, pcLimit: UInt32? = nil) {
var testData: Data?
if let filename = Bundle(for: type(of: self)).url(forResource: name, withExtension: "sfc") {
testData = try? Data(contentsOf: filename)
}
var testOutput: String?
if let filename = Bundle(for: type(of: self)).url(forResource: name + "-trace_compare", withExtension: "log") {
testOutput = try? String(contentsOf: filename)
}
XCTAssertNotNil(testData)
XCTAssertNotNil(testOutput)
let outputLines = testOutput!.components(separatedBy: "\r\n")
// Assumptions about the SFC file format follow; I couldn't find a spec but those
// produced by krom appear just to be binary dumps. Fingers crossed!
let machine = CSTestMachine6502(processor: .processor65816)
machine.setData(testData!, atAddress: 0x8000)
// This reproduces the state seen at the first line of all of LilaQ's traces;
// TODO: determine whether (i) this is the SNES state at reset, or merely how
// some sort of BIOS leaves it; and (ii) if the former, whether I have post-reset
// state incorrect. I strongly suspect it's a SNES-specific artefact?
machine.setValue(0x8000, for: .programCounter)
machine.setValue(0x0000, for: .A)
machine.setValue(0x0000, for: .X)
machine.setValue(0x0000, for: .Y)
machine.setValue(0x00ff, for: .stackPointer)
machine.setValue(0x34, for: .flags)
// There seems to be some Nintendo-special register at address 0x0000.
machine.setValue(0xb5, forAddress: 0x0000)
// Poke some fixed values for SNES registers to get past initial setup.
machine.setValue(0x42, forAddress: 0x4210) // "RDNMI", apparently; this says: CPU version 2, vblank interrupt request.
var allowNegativeError = false
var lineNumber = 1
var previousPC = 0
for line in outputLines {
// At least one of the traces ends with an empty line; my preference is not to
// modify the originals if possible.
if line == "" {
break
}
machine.runForNumber(ofInstructions: 1)
let newPC = Int(machine.value(for: .lastOperationAddress))
// Stop right now if a PC limit has been specified and this is it.
if let pcLimit = pcLimit, pcLimit == newPC {
break
}
func machineState() -> String {
// Formulate my 65816 state in the same form as the test machine
var cpuState = ""
let emulationFlag = machine.value(for: .emulationFlag) != 0
cpuState += String(format: "%06x ", machine.value(for: .lastOperationAddress))
cpuState += String(format: "A:%04x ", machine.value(for: .A))
cpuState += String(format: "X:%04x ", machine.value(for: .X))
cpuState += String(format: "Y:%04x ", machine.value(for: .Y))
if emulationFlag {
cpuState += String(format: "S:01%02x ", machine.value(for: .stackPointer))
} else {
cpuState += String(format: "S:%04x ", machine.value(for: .stackPointer))
}
cpuState += String(format: "D:%04x ", machine.value(for: .direct))
cpuState += String(format: "DB:%02x ", machine.value(for: .dataBank))
let flags = machine.value(for: .flags)
cpuState += (flags & 0x80) != 0 ? "N" : "n"
cpuState += (flags & 0x40) != 0 ? "V" : "v"
if emulationFlag {
cpuState += "1B"
} else {
cpuState += (flags & 0x20) != 0 ? "M" : "m"
cpuState += (flags & 0x10) != 0 ? "X" : "x"
}
cpuState += (flags & 0x08) != 0 ? "D" : "d"
cpuState += (flags & 0x04) != 0 ? "I" : "i"
cpuState += (flags & 0x02) != 0 ? "Z" : "z"
cpuState += (flags & 0x01) != 0 ? "C" : "c"
cpuState += " "
return cpuState
}
// Permit a fix-up of the negative flag only if this line followed a test of $4210.
var cpuState = machineState()
if cpuState != line && allowNegativeError {
machine.setValue(machine.value(for: .flags) ^ 0x80, for: .flags)
cpuState = machineState()
}
XCTAssertEqual(cpuState, line, "Mismatch on line #\(lineNumber); after instruction #\(String(format:"%02x", machine.value(forAddress: UInt32(previousPC))))")
if cpuState != line {
break
}
lineNumber += 1
previousPC = newPC
// Check whether a 'RDNMI' toggle needs to happen by peeking at the next instruction;
// if it's BIT $4210 then toggle the top bit at address $4210.
//
// Coupling here: assume that by the time the test 65816 is aware it's on a new instruction
// it's because the actual 65816 has read a new opcode, and that if the 65816 has just read
// a new opcode then it has already advanced the program counter.
let programCounter = machine.value(for: .programCounter)
let nextInstr = [
machine.value(forAddress: UInt32(programCounter - 1)),
machine.value(forAddress: UInt32(programCounter + 0)),
machine.value(forAddress: UInt32(programCounter + 1))
]
allowNegativeError = nextInstr[0] == 0x2c && nextInstr[1] == 0x10 && nextInstr[2] == 0x42
}
}
// MARK: - Tests
func testADC() { runTest("CPUADC") }
func testAND() { runTest("CPUAND") }
func testASL() { runTest("CPUASL") }
func testBIT() { runTest("CPUBIT") }
func testBRA() { runTest("CPUBRA") }
func testCMP() { runTest("CPUCMP") }
func testDEC() { runTest("CPUDEC") }
func testEOR() { runTest("CPUEOR") }
func testINC() { runTest("CPUINC") }
func testJMP() { runTest("CPUJMP") }
func testLDR() { runTest("CPULDR") }
func testLSR() { runTest("CPULSR") }
func testMOV() { runTest("CPUMOV") }
func testORA() { runTest("CPUORA") }
func testPHL() { runTest("CPUPHL") }
func testPSR() { runTest("CPUPSR") }
func testROL() { runTest("CPUROL") }
func testROR() { runTest("CPUROR") }
func testSBC() { runTest("CPUSBC") }
func testSTR() { runTest("CPUSTR") }
func testTRN() { runTest("CPUTRN") }
// Ensure the MSC tests stop before they attempt to test STP and WAI;
// the test relies on SNES means for scheduling a future interrupt.
func testMSC() { runTest("CPUMSC", pcLimit: 0x8523) }
}

View File

@ -39,6 +39,7 @@ extern const uint8_t CSTestMachine6502JamOpcode;
- (void)setData:(nonnull NSData *)data atAddress:(uint32_t)startAddress;
- (void)runForNumberOfCycles:(int)cycles;
- (void)runForNumberOfInstructions:(int)instructions;
- (void)setValue:(uint8_t)value forAddress:(uint32_t)address;
- (uint8_t)valueForAddress:(uint32_t)address;

View File

@ -114,4 +114,8 @@ static CPU::MOS6502::Register registerForRegister(CSTestMachine6502Register reg)
_processor->run_for(Cycles(cycles));
}
- (void)runForNumberOfInstructions:(int)instructions {
_processor->run_for_instructions(instructions);
}
@end

View File

@ -10,14 +10,14 @@ import XCTest
import Foundation
fileprivate func readHexInt16(from scanner: Scanner) -> UInt16 {
var temporary: UInt32 = 0
scanner.scanHexInt32(&temporary)
var temporary: UInt64 = 0
scanner.scanHexInt64(&temporary)
return UInt16(temporary)
}
fileprivate func readHexInt8(from scanner: Scanner) -> UInt8 {
var temporary: UInt32 = 0
scanner.scanHexInt32(&temporary)
var temporary: UInt64 = 0
scanner.scanHexInt64(&temporary)
return UInt8(temporary)
}

View File

@ -0,0 +1,231 @@
//
// IIgsMemoryMapTests.mm
// Clock SignalTests
//
// Created by Thomas Harte on 25/10/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
#include "../../../Machines/Apple/AppleIIgs/MemoryMap.hpp"
namespace {
using MemoryMap = Apple::IIgs::MemoryMap;
}
@interface IIgsMemoryMapTests : XCTestCase
@end
@implementation IIgsMemoryMapTests {
MemoryMap _memoryMap;
std::vector<uint8_t> _ram;
std::vector<uint8_t> _rom;
}
- (void)setUp {
_ram.resize((128 + 8 * 1024) * 1024);
_rom.resize(256 * 1024);
_memoryMap.set_storage(_ram, _rom);
// If this isn't the first test run, RAM and ROM may have old values.
// Initialise to a known state.
memset(_ram.data(), 0, _ram.size());
memset(_rom.data(), 0, _rom.size());
}
- (void)write:(uint8_t)value address:(uint32_t)address {
const auto &region = MemoryMapRegion(_memoryMap, address);
XCTAssertFalse(region.flags & MemoryMap::Region::IsIO);
MemoryMapWrite(_memoryMap, region, address, &value);
}
- (uint8_t)readAddress:(uint32_t)address {
const auto &region = MemoryMapRegion(_memoryMap, address);
uint8_t value;
MemoryMapRead(region, address, &value);
return value;
}
- (void)testAllRAM {
// Disable IO/LC 'shadowing', to give linear memory up to bank $80.
_memoryMap.set_shadow_register(0x5f);
// Fill memory via the map.
for(int address = 0x00'0000; address < 0x80'0000; ++address) {
const uint8_t value = uint8_t(address ^ (address >> 8) ^ (address >> 16));
[self write:value address:address];
}
// Test by direct access.
for(int address = 0x00'0000; address < 0x80'0000; ++address) {
const uint8_t value = uint8_t(address ^ (address >> 8) ^ (address >> 16));
XCTAssertEqual([self readAddress:address], value);
}
}
- (void)testROMIsReadonly {
_rom[0] = 0xc0;
// Test that ROM can be read in the correct location.
XCTAssertEqual([self readAddress:0xfc'0000], 0xc0);
// Try writing to it, and check that nothing happened.
[self write:0xfc address:0xfc'0000];
XCTAssertEqual(_rom[0], 0xc0);
}
/// Tests that the same portion of ROM is visible in banks $00, $01, $e0 and $e1.
- (void)testROMVisibility {
_rom.back() = 0xa8;
auto test_bank = [self](uint32_t bank) {
const uint32_t address = bank | 0xffff;
XCTAssertEqual([self readAddress:address], 0xa8);
};
test_bank(0x00'0000);
test_bank(0x01'0000);
test_bank(0xe0'0000);
test_bank(0xe1'0000);
}
/// Tests that writes to $00:$0400 and to $01:$0400 are subsequently visible at $e0:$0400 and $e1:$0400.
- (void)testShadowing {
[self write:0xab address:0x00'0400];
[self write:0xcd address:0x01'0400];
XCTAssertEqual([self readAddress:0xe0'0400], 0xab);
XCTAssertEqual([self readAddress:0xe1'0400], 0xcd);
}
/// Tests that a write to bank $00 which via the auxiliary switches is redirected to bank $01 is then
/// mirrored to $e1.
- (void)testAuxiliaryShadowing {
// Select the alternate text page 1.
_memoryMap.access(0xc001, false); // Set 80STORE.
_memoryMap.access(0xc055, false); // Set PAGE2.
// These two things together should enable auxiliary memory for text page 1.
// No, really.
// Enable shadowing of text page 1.
_memoryMap.set_shadow_register(0x00);
// Establish a different value in bank $e1, then write
// to bank $00 and check banks $01 and $e1.
[self write: 0xcb address:0xe1'0400];
[self write: 0xde address:0x00'0400];
XCTAssertEqual([self readAddress:0xe1'0400], 0xde);
XCTAssertEqual([self readAddress:0x01'0400], 0xde);
// Reset the $e1 page version and check all three detinations.
[self write: 0xcb address:0xe1'0400];
XCTAssertEqual([self readAddress:0xe1'0400], 0xcb);
XCTAssertEqual([self readAddress:0x00'0400], 0xde);
XCTAssertEqual([self readAddress:0x01'0400], 0xde);
}
- (void)testE0E1RAMConsistent {
// Do some random language card paging, to hit set_language_card.
_memoryMap.set_state_register(0x00);
_memoryMap.set_state_register(0xff);
[self write: 0x12 address:0xe0'0000];
[self write: 0x34 address:0xe1'0000];
XCTAssertEqual(_ram[_ram.size() - 128*1024], 0x12);
XCTAssertEqual(_ram[_ram.size() - 64*1024], 0x34);
}
- (void)testAuxiliarySwitches {
// Inhibit IO/LC 'shadowing'.
_memoryMap.set_shadow_register(0x40);
// Check that all writes and reads currently occur to main RAM.
XCTAssertEqual(_memoryMap.get_state_register() & 0xf0, 0x00);
for(int c = 0; c < 65536; c++) {
const uint8_t value = c ^ (c >> 8);
[self write:value address:c];
XCTAssertEqual(_ram[c], value);
}
// Reset.
memset(_ram.data(), 0, 128*1024);
// Set writing to auxiliary memory.
// Reading should still be from main.
_memoryMap.access(0xc005, false);
XCTAssertEqual(_memoryMap.get_state_register() & 0xf0, 0x10);
for(int c = 0x0200; c < 0xc000; c++) {
const uint8_t value = c ^ (c >> 8);
[self write:value address:c];
XCTAssertEqual(_ram[c + 64*1024], value);
XCTAssertEqual([self readAddress:c], 0);
}
// Reset.
memset(_ram.data(), 0, 128*1024);
// Switch reading and writing.
_memoryMap.access(0xc004, false);
_memoryMap.access(0xc003, false);
XCTAssertEqual(_memoryMap.get_state_register() & 0xf0, 0x20);
for(int c = 0x0200; c < 0xc000; c++) {
const uint8_t value = c ^ (c >> 8);
[self write:value address:c];
XCTAssertEqual(_ram[c], value);
XCTAssertEqual([self readAddress:c], 0);
}
// Reset.
memset(_ram.data(), 0, 128*1024);
// Test main zero page.
for(int c = 0x0000; c < 0x0200; c++) {
const uint8_t value = c ^ (c >> 8);
[self write:value address:c];
XCTAssertEqual(_ram[c], value);
XCTAssertEqual([self readAddress:c], value);
}
// Reset.
memset(_ram.data(), 0, 128*1024);
// Enable the alternate zero page.
_memoryMap.access(0xc009, false);
XCTAssertEqual(_memoryMap.get_state_register() & 0xf0, 0xa0);
for(int c = 0x0000; c < 0x0200; c++) {
const uint8_t value = c ^ (c >> 8);
[self write:value address:c];
XCTAssertEqual(_ram[c + 64*1024], value);
XCTAssertEqual([self readAddress:c], value);
}
// Reset.
memset(_ram.data(), 0, 128*1024);
// Enable 80STORE and PAGE2 and test for access to the second video page.
_memoryMap.access(0xc001, false);
_memoryMap.access(0xc055, true);
XCTAssertEqual(_memoryMap.get_state_register() & 0xf0, 0xe0);
for(int c = 0x0400; c < 0x0800; c++) {
const uint8_t value = c ^ (c >> 8);
[self write:value address:c];
XCTAssertEqual(_ram[c + 64*1024], value);
XCTAssertEqual([self readAddress:c], value);
}
// Reset.
memset(_ram.data(), 0, 128*1024);
// Enable HIRES and test for access to the second video page.
_memoryMap.access(0xc057, true);
for(int c = 0x2000; c < 0x4000; c++) {
const uint8_t value = c ^ (c >> 8);
[self write:value address:c];
XCTAssertEqual(_ram[c + 64*1024], value);
XCTAssertEqual([self readAddress:c], value);
}
}
@end

View File

@ -13,29 +13,10 @@
@interface MasterSystemVDPTests : XCTestCase
@end
@implementation MasterSystemVDPTests {
NSOpenGLContext *_openGLContext;
}
@implementation MasterSystemVDPTests
- (void)setUp {
[super setUp];
// Create a valid OpenGL context, so that a VDP can be constructed.
NSOpenGLPixelFormatAttribute attributes[] = {
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
0
};
NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes];
_openGLContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil];
[_openGLContext makeCurrentContext];
}
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
_openGLContext = nil;
[super tearDown];
}
- (void)testLineInterrupt {

View File

@ -11,8 +11,8 @@
#include "../../../InstructionSets/PowerPC/Decoder.hpp"
namespace {
using Operation = CPU::Decoder::PowerPC::Operation;
using Instruction = CPU::Decoder::PowerPC::Instruction;
using Operation = InstructionSet::PowerPC::Operation;
using Instruction = InstructionSet::PowerPC::Instruction;
}
@interface PowerPCDecoderTests : XCTestCase
@ -119,7 +119,7 @@ namespace {
// MARK: - Decoder
- (void)decode:(const uint32_t *)stream {
CPU::Decoder::PowerPC::Decoder decoder(CPU::Decoder::PowerPC::Model::MPC601);
InstructionSet::PowerPC::Decoder decoder(InstructionSet::PowerPC::Model::MPC601);
for(int c = 0; c < 32; c++) {
instructions[c] = decoder.decode(stream[c]);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More