mirror of
https://github.com/TomHarte/CLK.git
synced 2025-09-12 02:24:31 +00:00
Compare commits
13 Commits
Indentatio
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
cad42beef4 | ||
|
96fd0b7892 | ||
|
6f1db15d7c | ||
|
1854296ee8 | ||
|
515cc5f326 | ||
|
091be7eafe | ||
|
27a19ea417 | ||
|
9a5e9af67c | ||
|
3a493f2428 | ||
|
ca6e34f4b4 | ||
|
e1e68312c4 | ||
|
c7ff2cece4 | ||
|
8e6f4fa36f |
@@ -23,10 +23,10 @@ struct Machine {
|
||||
|
||||
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
|
||||
friend Configurable::DisplayOption<Options>;
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Configurable::Display::SVideo : Configurable::Display::CompositeColour) {}
|
||||
public:
|
||||
Options(Configurable::OptionsType type) :
|
||||
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||
Configurable::Display::SVideo : Configurable::Display::CompositeColour) {}
|
||||
|
||||
private:
|
||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||
|
@@ -44,37 +44,37 @@ namespace {
|
||||
Log::Logger<Log::Source::Plus4> logger;
|
||||
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Up),
|
||||
Input(Input::Down),
|
||||
Input(Input::Left),
|
||||
Input(Input::Right),
|
||||
Input(Input::Fire)
|
||||
}) {}
|
||||
public:
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Up),
|
||||
Input(Input::Down),
|
||||
Input(Input::Left),
|
||||
Input(Input::Right),
|
||||
Input(Input::Fire)
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
const auto apply = [&](uint8_t mask) {
|
||||
if(is_active) mask_ &= ~mask; else mask_ |= mask;
|
||||
};
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
const auto apply = [&](uint8_t mask) {
|
||||
if(is_active) mask_ &= ~mask; else mask_ |= mask;
|
||||
};
|
||||
|
||||
switch(digital_input.type) {
|
||||
default: return;
|
||||
case Input::Right: apply(0x08); break;
|
||||
case Input::Left: apply(0x04); break;
|
||||
case Input::Down: apply(0x02); break;
|
||||
case Input::Up: apply(0x01); break;
|
||||
case Input::Fire: apply(0xc0); break;
|
||||
}
|
||||
switch(digital_input.type) {
|
||||
default: return;
|
||||
case Input::Right: apply(0x08); break;
|
||||
case Input::Left: apply(0x04); break;
|
||||
case Input::Down: apply(0x02); break;
|
||||
case Input::Up: apply(0x01); break;
|
||||
case Input::Fire: apply(0xc0); break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t mask() const {
|
||||
return mask_;
|
||||
}
|
||||
uint8_t mask() const {
|
||||
return mask_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t mask_ = 0xff;
|
||||
private:
|
||||
uint8_t mask_ = 0xff;
|
||||
};
|
||||
|
||||
class Timers {
|
||||
|
@@ -13,35 +13,35 @@
|
||||
namespace MSX::Cartridge {
|
||||
|
||||
class ASCII16kbROMSlotHandler: public MemorySlotHandler {
|
||||
public:
|
||||
ASCII16kbROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
||||
public:
|
||||
ASCII16kbROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
||||
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
||||
switch(address >> 11) {
|
||||
default:
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
break;
|
||||
case 0xc:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0x6000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x4000, 0x4000, 0x4000);
|
||||
break;
|
||||
case 0xe:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0x7000 || address == 0x77ff) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x4000, 0x8000, 0x4000);
|
||||
break;
|
||||
}
|
||||
void write(const uint16_t address, const uint8_t value, const bool pc_is_outside_bios) final {
|
||||
switch(address >> 11) {
|
||||
default:
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
break;
|
||||
case 0xc:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0x6000);
|
||||
}
|
||||
slot_.map(value * 0x4000, 0x4000, 0x4000);
|
||||
break;
|
||||
case 0xe:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0x7000 || address == 0x77ff);
|
||||
}
|
||||
slot_.map(value * 0x4000, 0x8000, 0x4000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
virtual std::string debug_type() final {
|
||||
return "A16";
|
||||
}
|
||||
virtual std::string debug_type() final {
|
||||
return "A16";
|
||||
}
|
||||
|
||||
private:
|
||||
MSX::MemorySlot &slot_;
|
||||
private:
|
||||
MSX::MemorySlot &slot_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -13,47 +13,47 @@
|
||||
namespace MSX::Cartridge {
|
||||
|
||||
class ASCII8kbROMSlotHandler: public MemorySlotHandler {
|
||||
public:
|
||||
ASCII8kbROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
||||
public:
|
||||
ASCII8kbROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
||||
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
||||
switch(address >> 11) {
|
||||
default:
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
break;
|
||||
case 0xc:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0x6000 || address == 0x60ff) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x4000, 0x2000);
|
||||
break;
|
||||
case 0xd:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0x6800 || address == 0x68ff) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x6000, 0x2000);
|
||||
break;
|
||||
case 0xe:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0x7000 || address == 0x70ff) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x8000, 0x2000);
|
||||
break;
|
||||
case 0xf:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0x7800 || address == 0x78ff) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x2000, 0xa000, 0x2000);
|
||||
break;
|
||||
}
|
||||
void write(const uint16_t address, const uint8_t value, const bool pc_is_outside_bios) final {
|
||||
switch(address >> 11) {
|
||||
default:
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
break;
|
||||
case 0xc:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0x6000 || address == 0x60ff);
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x4000, 0x2000);
|
||||
break;
|
||||
case 0xd:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0x6800 || address == 0x68ff);
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x6000, 0x2000);
|
||||
break;
|
||||
case 0xe:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0x7000 || address == 0x70ff);
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x8000, 0x2000);
|
||||
break;
|
||||
case 0xf:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0x7800 || address == 0x78ff);
|
||||
}
|
||||
slot_.map(value * 0x2000, 0xa000, 0x2000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
virtual std::string debug_type() final {
|
||||
return "A8";
|
||||
}
|
||||
virtual std::string debug_type() final {
|
||||
return "A8";
|
||||
}
|
||||
|
||||
private:
|
||||
MSX::MemorySlot &slot_;
|
||||
private:
|
||||
MSX::MemorySlot &slot_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -13,39 +13,40 @@
|
||||
namespace MSX::Cartridge {
|
||||
|
||||
class KonamiROMSlotHandler: public MemorySlotHandler {
|
||||
public:
|
||||
KonamiROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
||||
public:
|
||||
KonamiROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
||||
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
||||
switch(address >> 13) {
|
||||
default:
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
break;
|
||||
case 3:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0x6000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x6000, 0x2000);
|
||||
break;
|
||||
case 4:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0x8000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x8000, 0x2000);
|
||||
break;
|
||||
case 5:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0xa000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x2000, 0xa000, 0x2000);
|
||||
break;
|
||||
}
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
||||
switch(address >> 13) {
|
||||
default:
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
break;
|
||||
case 3:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0x6000);
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x6000, 0x2000);
|
||||
break;
|
||||
case 4:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0x8000);
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x8000, 0x2000);
|
||||
break;
|
||||
case 5:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0xa000);
|
||||
}
|
||||
slot_.map(value * 0x2000, 0xa000, 0x2000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
virtual std::string debug_type() final {
|
||||
return "K";
|
||||
}
|
||||
private:
|
||||
virtual std::string debug_type() final {
|
||||
return "K";
|
||||
}
|
||||
|
||||
private:
|
||||
MSX::MemorySlot &slot_;
|
||||
};
|
||||
|
||||
|
@@ -14,73 +14,73 @@
|
||||
namespace MSX::Cartridge {
|
||||
|
||||
class KonamiWithSCCROMSlotHandler: public MemorySlotHandler {
|
||||
public:
|
||||
KonamiWithSCCROMSlotHandler(MSX::MemorySlot &slot, Konami::SCC &scc) :
|
||||
slot_(slot), scc_(scc) {}
|
||||
public:
|
||||
KonamiWithSCCROMSlotHandler(MSX::MemorySlot &slot, Konami::SCC &scc) :
|
||||
slot_(slot), scc_(scc) {}
|
||||
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
||||
switch(address >> 11) {
|
||||
default:
|
||||
void write(const uint16_t address, const uint8_t value, const bool pc_is_outside_bios) final {
|
||||
switch(address >> 11) {
|
||||
default:
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
break;
|
||||
case 0x0a:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0x5000);
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x4000, 0x2000);
|
||||
break;
|
||||
case 0x0e:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0x7000);
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x6000, 0x2000);
|
||||
break;
|
||||
case 0x12:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0x9000);
|
||||
}
|
||||
if((value&0x3f) == 0x3f) {
|
||||
scc_is_visible_ = true;
|
||||
slot_.map_handler(0x8000, 0x2000);
|
||||
} else {
|
||||
scc_is_visible_ = false;
|
||||
slot_.map(value * 0x2000, 0x8000, 0x2000);
|
||||
}
|
||||
break;
|
||||
case 0x13:
|
||||
if(scc_is_visible_) {
|
||||
if(pc_is_outside_bios) confidence_counter_.add_hit();
|
||||
scc_.write(address, value);
|
||||
} else {
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
break;
|
||||
case 0x0a:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0x5000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x4000, 0x2000);
|
||||
break;
|
||||
case 0x0e:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0x7000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x2000, 0x6000, 0x2000);
|
||||
break;
|
||||
case 0x12:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0x9000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
if((value&0x3f) == 0x3f) {
|
||||
scc_is_visible_ = true;
|
||||
slot_.map_handler(0x8000, 0x2000);
|
||||
} else {
|
||||
scc_is_visible_ = false;
|
||||
slot_.map(value * 0x2000, 0x8000, 0x2000);
|
||||
}
|
||||
break;
|
||||
case 0x13:
|
||||
if(scc_is_visible_) {
|
||||
if(pc_is_outside_bios) confidence_counter_.add_hit();
|
||||
scc_.write(address, value);
|
||||
} else {
|
||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||
}
|
||||
break;
|
||||
case 0x16:
|
||||
if(pc_is_outside_bios) {
|
||||
if(address == 0xb000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
||||
}
|
||||
slot_.map(value * 0x2000, 0xa000, 0x2000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0x16:
|
||||
if(pc_is_outside_bios) {
|
||||
hit_or_equivocal(address == 0xb000);
|
||||
}
|
||||
slot_.map(value * 0x2000, 0xa000, 0x2000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t read(uint16_t address) final {
|
||||
if(scc_is_visible_ && address >= 0x9800 && address < 0xa000) {
|
||||
confidence_counter_.add_hit();
|
||||
return scc_.read(address);
|
||||
}
|
||||
confidence_counter_.add_miss();
|
||||
return 0xff;
|
||||
uint8_t read(uint16_t address) final {
|
||||
if(scc_is_visible_ && address >= 0x9800 && address < 0xa000) {
|
||||
confidence_counter_.add_hit();
|
||||
return scc_.read(address);
|
||||
}
|
||||
confidence_counter_.add_miss();
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
virtual std::string debug_type() final {
|
||||
return "KSCC";
|
||||
}
|
||||
virtual std::string debug_type() final {
|
||||
return "KSCC";
|
||||
}
|
||||
|
||||
private:
|
||||
MSX::MemorySlot &slot_;
|
||||
Konami::SCC &scc_;
|
||||
bool scc_is_visible_ = false;
|
||||
private:
|
||||
MSX::MemorySlot &slot_;
|
||||
Konami::SCC &scc_;
|
||||
bool scc_is_visible_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -21,22 +21,22 @@
|
||||
namespace MSX {
|
||||
|
||||
class DiskROM: public MemorySlotHandler, public WD::WD1770 {
|
||||
public:
|
||||
DiskROM(MSX::MemorySlot &slot);
|
||||
public:
|
||||
DiskROM(MSX::MemorySlot &slot);
|
||||
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final;
|
||||
uint8_t read(uint16_t address) final;
|
||||
void run_for(HalfCycles half_cycles) final;
|
||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final;
|
||||
uint8_t read(uint16_t address) final;
|
||||
void run_for(HalfCycles half_cycles) final;
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
|
||||
private:
|
||||
const std::vector<uint8_t> &rom_;
|
||||
private:
|
||||
const std::vector<uint8_t> &rom_;
|
||||
|
||||
long int controller_cycles_ = 0;
|
||||
long int controller_cycles_ = 0;
|
||||
|
||||
void set_head_load_request(bool head_load) final;
|
||||
void set_head_load_request(bool head_load) final;
|
||||
};
|
||||
|
||||
}
|
||||
|
1668
Machines/MSX/MSX.cpp
1668
Machines/MSX/MSX.cpp
File diff suppressed because it is too large
Load Diff
@@ -34,115 +34,122 @@ struct MemorySlotChangeHandler {
|
||||
};
|
||||
|
||||
class MemorySlot {
|
||||
public:
|
||||
MemorySlot(MemorySlotChangeHandler &);
|
||||
public:
|
||||
MemorySlot(MemorySlotChangeHandler &);
|
||||
|
||||
/// @returns A pointer to the area of memory currently underneath @c address that
|
||||
/// should be read
|
||||
const uint8_t *read_pointer(int segment) const;
|
||||
/// @returns A pointer to the area of memory currently underneath @c address that
|
||||
/// should be read
|
||||
const uint8_t *read_pointer(int segment) const;
|
||||
|
||||
/// @returns A pointer to the area of memory currently underneath @c address.
|
||||
uint8_t *write_pointer(int segment) const;
|
||||
/// @returns A pointer to the area of memory currently underneath @c address.
|
||||
uint8_t *write_pointer(int segment) const;
|
||||
|
||||
/// Copies an underlying source buffer.
|
||||
void set_source(const std::vector<uint8_t> &source);
|
||||
/// Copies an underlying source buffer.
|
||||
void set_source(const std::vector<uint8_t> &source);
|
||||
|
||||
/// Sets the size of the underlying source buffer.
|
||||
void resize_source(std::size_t);
|
||||
/// Sets the size of the underlying source buffer.
|
||||
void resize_source(std::size_t);
|
||||
|
||||
/// Provides a reference to the internal source storage.
|
||||
std::vector<uint8_t> &source();
|
||||
const std::vector<uint8_t> &source() const;
|
||||
/// Provides a reference to the internal source storage.
|
||||
std::vector<uint8_t> &source();
|
||||
const std::vector<uint8_t> &source() const;
|
||||
|
||||
enum AccessType {
|
||||
Read,
|
||||
ReadWrite
|
||||
};
|
||||
enum AccessType {
|
||||
Read,
|
||||
ReadWrite
|
||||
};
|
||||
|
||||
/// Maps the content from @c source_address in the buffer previously
|
||||
/// supplied to @c set_source to the region indicated by
|
||||
/// @c destination_address and @c length within @c subslot.
|
||||
template <AccessType type = AccessType::Read> void map(
|
||||
std::size_t source_address,
|
||||
uint16_t destination_address,
|
||||
std::size_t length);
|
||||
/// Maps the content from @c source_address in the buffer previously
|
||||
/// supplied to @c set_source to the region indicated by
|
||||
/// @c destination_address and @c length within @c subslot.
|
||||
template <AccessType type = AccessType::Read> void map(
|
||||
std::size_t source_address,
|
||||
uint16_t destination_address,
|
||||
std::size_t length);
|
||||
|
||||
/// Marks the region indicated by @c destination_address and @c length
|
||||
/// as requiring calls into this slot's MemorySlotHandler.
|
||||
void map_handler(
|
||||
uint16_t destination_address,
|
||||
std::size_t length);
|
||||
/// Marks the region indicated by @c destination_address and @c length
|
||||
/// as requiring calls into this slot's MemorySlotHandler.
|
||||
void map_handler(
|
||||
uint16_t destination_address,
|
||||
std::size_t length);
|
||||
|
||||
/// Marks the region indicated by @c destination_address and @c length
|
||||
/// as unoccupied.
|
||||
void unmap(
|
||||
uint16_t destination_address,
|
||||
std::size_t length);
|
||||
/// Marks the region indicated by @c destination_address and @c length
|
||||
/// as unoccupied.
|
||||
void unmap(
|
||||
uint16_t destination_address,
|
||||
std::size_t length);
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> source_;
|
||||
uint8_t *read_pointers_[8];
|
||||
uint8_t *write_pointers_[8];
|
||||
private:
|
||||
std::vector<uint8_t> source_;
|
||||
uint8_t *read_pointers_[8];
|
||||
uint8_t *write_pointers_[8];
|
||||
|
||||
MemorySlotChangeHandler &handler_;
|
||||
MemorySlotChangeHandler &handler_;
|
||||
|
||||
using MemoryChunk = std::array<uint8_t, 8192>;
|
||||
inline static MemoryChunk unmapped{0xff};
|
||||
inline static MemoryChunk scratch;
|
||||
using MemoryChunk = std::array<uint8_t, 8192>;
|
||||
inline static MemoryChunk unmapped{0xff};
|
||||
inline static MemoryChunk scratch;
|
||||
};
|
||||
|
||||
class PrimarySlot {
|
||||
public:
|
||||
PrimarySlot(MemorySlotChangeHandler &);
|
||||
public:
|
||||
PrimarySlot(MemorySlotChangeHandler &);
|
||||
|
||||
/// @returns A pointer to the area of memory currently underneath @c address that
|
||||
/// should be read
|
||||
const uint8_t *read_pointer(int segment) const;
|
||||
/// @returns A pointer to the area of memory currently underneath @c address that
|
||||
/// should be read
|
||||
const uint8_t *read_pointer(int segment) const;
|
||||
|
||||
/// @returns A pointer to the area of memory currently underneath @c address.
|
||||
uint8_t *write_pointer(int segment) const;
|
||||
/// @returns A pointer to the area of memory currently underneath @c address.
|
||||
uint8_t *write_pointer(int segment) const;
|
||||
|
||||
/// Attempts to write the argument as the secondary paging selection.
|
||||
void set_secondary_paging(uint8_t);
|
||||
/// Attempts to write the argument as the secondary paging selection.
|
||||
void set_secondary_paging(uint8_t);
|
||||
|
||||
/// @returns The value most recently provided to @c set_secondary_paging.
|
||||
uint8_t secondary_paging() const;
|
||||
/// @returns The value most recently provided to @c set_secondary_paging.
|
||||
uint8_t secondary_paging() const;
|
||||
|
||||
/// Indicates whether this slot supports secondary paging.
|
||||
bool supports_secondary_paging = false;
|
||||
/// Indicates whether this slot supports secondary paging.
|
||||
bool supports_secondary_paging = false;
|
||||
|
||||
/// Provides the subslot at the specified index.
|
||||
MemorySlot &subslot(int);
|
||||
/// Provides the subslot at the specified index.
|
||||
MemorySlot &subslot(int);
|
||||
|
||||
private:
|
||||
MemorySlot subslots_[4];
|
||||
uint8_t secondary_paging_ = 0;
|
||||
private:
|
||||
MemorySlot subslots_[4];
|
||||
uint8_t secondary_paging_ = 0;
|
||||
};
|
||||
|
||||
class MemorySlotHandler {
|
||||
public:
|
||||
virtual ~MemorySlotHandler() = default;
|
||||
public:
|
||||
virtual ~MemorySlotHandler() = default;
|
||||
|
||||
/*! Advances time by @c half_cycles. */
|
||||
virtual void run_for([[maybe_unused]] HalfCycles half_cycles) {}
|
||||
/*! Advances time by @c half_cycles. */
|
||||
virtual void run_for([[maybe_unused]] HalfCycles half_cycles) {}
|
||||
|
||||
/*! Announces an attempt to write @c value to @c address. */
|
||||
virtual void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) = 0;
|
||||
/*! Announces an attempt to write @c value to @c address. */
|
||||
virtual void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) = 0;
|
||||
|
||||
/*! Seeks the result of a read at @c address; this is used only if the area is unmapped. */
|
||||
virtual uint8_t read([[maybe_unused]] uint16_t address) { return 0xff; }
|
||||
/*! Seeks the result of a read at @c address; this is used only if the area is unmapped. */
|
||||
virtual uint8_t read([[maybe_unused]] uint16_t address) { return 0xff; }
|
||||
|
||||
/*! @returns The probability that this handler is correct for the data it owns. */
|
||||
float get_confidence() {
|
||||
return confidence_counter_.confidence();
|
||||
/*! @returns The probability that this handler is correct for the data it owns. */
|
||||
float get_confidence() {
|
||||
return confidence_counter_.confidence();
|
||||
}
|
||||
|
||||
virtual std::string debug_type() {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected:
|
||||
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
|
||||
void hit_or_equivocal(const bool is_hit) {
|
||||
if(is_hit) {
|
||||
confidence_counter_.add_hit();
|
||||
} else {
|
||||
confidence_counter_.add_equivocal();
|
||||
}
|
||||
|
||||
virtual std::string debug_type() {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected:
|
||||
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -40,42 +40,42 @@ namespace Sega {
|
||||
namespace MasterSystem {
|
||||
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Up),
|
||||
Input(Input::Down),
|
||||
Input(Input::Left),
|
||||
Input(Input::Right),
|
||||
public:
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Up),
|
||||
Input(Input::Down),
|
||||
Input(Input::Left),
|
||||
Input(Input::Right),
|
||||
|
||||
Input(Input::Fire, 0),
|
||||
Input(Input::Fire, 1)
|
||||
}) {}
|
||||
Input(Input::Fire, 0),
|
||||
Input(Input::Fire, 1)
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
switch(digital_input.type) {
|
||||
default: return;
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
switch(digital_input.type) {
|
||||
default: return;
|
||||
|
||||
case Input::Up: if(is_active) state_ &= ~0x01; else state_ |= 0x01; break;
|
||||
case Input::Down: if(is_active) state_ &= ~0x02; else state_ |= 0x02; break;
|
||||
case Input::Left: if(is_active) state_ &= ~0x04; else state_ |= 0x04; break;
|
||||
case Input::Right: if(is_active) state_ &= ~0x08; else state_ |= 0x08; break;
|
||||
case Input::Fire:
|
||||
switch(digital_input.info.control.index) {
|
||||
default: break;
|
||||
case 0: if(is_active) state_ &= ~0x10; else state_ |= 0x10; break;
|
||||
case 1: if(is_active) state_ &= ~0x20; else state_ |= 0x20; break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Input::Up: if(is_active) state_ &= ~0x01; else state_ |= 0x01; break;
|
||||
case Input::Down: if(is_active) state_ &= ~0x02; else state_ |= 0x02; break;
|
||||
case Input::Left: if(is_active) state_ &= ~0x04; else state_ |= 0x04; break;
|
||||
case Input::Right: if(is_active) state_ &= ~0x08; else state_ |= 0x08; break;
|
||||
case Input::Fire:
|
||||
switch(digital_input.info.control.index) {
|
||||
default: break;
|
||||
case 0: if(is_active) state_ &= ~0x10; else state_ |= 0x10; break;
|
||||
case 1: if(is_active) state_ &= ~0x20; else state_ |= 0x20; break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t get_state() {
|
||||
return state_;
|
||||
}
|
||||
uint8_t get_state() {
|
||||
return state_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t state_ = 0xff;
|
||||
private:
|
||||
uint8_t state_ = 0xff;
|
||||
};
|
||||
|
||||
template <Analyser::Static::Sega::Target::Model model> class ConcreteMachine:
|
||||
@@ -90,282 +90,282 @@ template <Analyser::Static::Sega::Target::Model model> class ConcreteMachine:
|
||||
public MachineTypes::ScanProducer,
|
||||
public MachineTypes::TimedMachine {
|
||||
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::Sega::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
region_(target.region),
|
||||
paging_scheme_(target.paging_scheme),
|
||||
z80_(*this),
|
||||
sn76489_(
|
||||
(target.model == Target::Model::SG1000) ? TI::SN76489::Personality::SN76489 : TI::SN76489::Personality::SMS,
|
||||
audio_queue_,
|
||||
audio_divider),
|
||||
opll_(audio_queue_, audio_divider),
|
||||
mixer_(sn76489_, opll_),
|
||||
speaker_(mixer_),
|
||||
keyboard_({Inputs::Keyboard::Key::Enter, Inputs::Keyboard::Key::Escape}, {}) {
|
||||
// Pick the clock rate based on the region.
|
||||
const double clock_rate = target.region == Target::Region::Europe ? 3546893.0 : 3579540.0;
|
||||
speaker_.set_input_rate(float(clock_rate / audio_divider));
|
||||
set_clock_rate(clock_rate);
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::Sega::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
region_(target.region),
|
||||
paging_scheme_(target.paging_scheme),
|
||||
z80_(*this),
|
||||
sn76489_(
|
||||
(target.model == Target::Model::SG1000) ? TI::SN76489::Personality::SN76489 : TI::SN76489::Personality::SMS,
|
||||
audio_queue_,
|
||||
audio_divider),
|
||||
opll_(audio_queue_, audio_divider),
|
||||
mixer_(sn76489_, opll_),
|
||||
speaker_(mixer_),
|
||||
keyboard_({Inputs::Keyboard::Key::Enter, Inputs::Keyboard::Key::Escape}, {}) {
|
||||
// Pick the clock rate based on the region.
|
||||
const double clock_rate = target.region == Target::Region::Europe ? 3546893.0 : 3579540.0;
|
||||
speaker_.set_input_rate(float(clock_rate / audio_divider));
|
||||
set_clock_rate(clock_rate);
|
||||
|
||||
// Instantiate the joysticks.
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
// Instantiate the joysticks.
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
|
||||
// Clear the memory map.
|
||||
map(read_pointers_, nullptr, 0x10000, 0);
|
||||
map(write_pointers_, nullptr, 0x10000, 0);
|
||||
// Clear the memory map.
|
||||
map(read_pointers_, nullptr, 0x10000, 0);
|
||||
map(write_pointers_, nullptr, 0x10000, 0);
|
||||
|
||||
// Take a copy of the cartridge and place it into memory.
|
||||
if(!target.media.cartridges.empty()) {
|
||||
cartridge_ = target.media.cartridges[0]->get_segments()[0].data;
|
||||
}
|
||||
if(cartridge_.size() < 48*1024) {
|
||||
std::size_t new_space = 48*1024 - cartridge_.size();
|
||||
cartridge_.resize(48*1024);
|
||||
memset(&cartridge_[48*1024 - new_space], 0xff, new_space);
|
||||
}
|
||||
|
||||
if(paging_scheme_ == Target::PagingScheme::Codemasters) {
|
||||
// The Codemasters cartridges start with pages 0, 1 and 0 again initially visible.
|
||||
paging_registers_[0] = 0;
|
||||
paging_registers_[1] = 1;
|
||||
paging_registers_[2] = 0;
|
||||
}
|
||||
|
||||
// Load the BIOS if available.
|
||||
//
|
||||
// TODO: there's probably a million other versions of the Master System BIOS; try to build a
|
||||
// CRC32 catalogue of those. So far:
|
||||
//
|
||||
// 0072ed54 = US/European BIOS 1.3
|
||||
// 48d44a13 = Japanese BIOS 2.1
|
||||
const bool is_japanese = target.region == Target::Region::Japan;
|
||||
const ROM::Name bios_name = is_japanese ? ROM::Name::MasterSystemJapaneseBIOS : ROM::Name::MasterSystemWesternBIOS;
|
||||
ROM::Request request(bios_name, true);
|
||||
auto roms = rom_fetcher(request);
|
||||
request.validate(roms);
|
||||
|
||||
const auto rom = roms.find(bios_name);
|
||||
if(rom == roms.end()) {
|
||||
// No BIOS found; attempt to boot as though it has already disabled itself.
|
||||
has_bios_ = false;
|
||||
memory_control_ |= 0x08;
|
||||
std::cerr << "No BIOS found; attempting to start cartridge directly" << std::endl;
|
||||
} else {
|
||||
has_bios_ = true;
|
||||
memcpy(&bios_, rom->second.data(), std::min(sizeof(bios_), rom->second.size()));
|
||||
}
|
||||
page_cartridge();
|
||||
|
||||
// Map RAM.
|
||||
if constexpr (is_master_system(model)) {
|
||||
map(read_pointers_, ram_, 8*1024, 0xc000, 0x10000);
|
||||
map(write_pointers_, ram_, 8*1024, 0xc000, 0x10000);
|
||||
} else {
|
||||
map(read_pointers_, ram_, 1024, 0xc000, 0x10000);
|
||||
map(write_pointers_, ram_, 1024, 0xc000, 0x10000);
|
||||
}
|
||||
|
||||
// Apply a relatively low low-pass filter. More guidance needed here.
|
||||
// TODO: this is disabled for now since it isn't applicable for the FM chip, I think.
|
||||
// speaker_.set_high_frequency_cutoff(8000);
|
||||
|
||||
// Set default mixer levels: FM off, SN full-throttle.
|
||||
set_mixer_levels(0);
|
||||
|
||||
keyboard_.set_delegate(this);
|
||||
// Take a copy of the cartridge and place it into memory.
|
||||
if(!target.media.cartridges.empty()) {
|
||||
cartridge_ = target.media.cartridges[0]->get_segments()[0].data;
|
||||
}
|
||||
if(cartridge_.size() < 48*1024) {
|
||||
std::size_t new_space = 48*1024 - cartridge_.size();
|
||||
cartridge_.resize(48*1024);
|
||||
memset(&cartridge_[48*1024 - new_space], 0xff, new_space);
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
if(paging_scheme_ == Target::PagingScheme::Codemasters) {
|
||||
// The Codemasters cartridges start with pages 0, 1 and 0 again initially visible.
|
||||
paging_registers_[0] = 0;
|
||||
paging_registers_[1] = 1;
|
||||
paging_registers_[2] = 0;
|
||||
}
|
||||
|
||||
ChangeEffect effect_for_file_did_change(const std::string &) const final {
|
||||
return ChangeEffect::RestartMachine;
|
||||
// Load the BIOS if available.
|
||||
//
|
||||
// TODO: there's probably a million other versions of the Master System BIOS; try to build a
|
||||
// CRC32 catalogue of those. So far:
|
||||
//
|
||||
// 0072ed54 = US/European BIOS 1.3
|
||||
// 48d44a13 = Japanese BIOS 2.1
|
||||
const bool is_japanese = target.region == Target::Region::Japan;
|
||||
const ROM::Name bios_name = is_japanese ? ROM::Name::MasterSystemJapaneseBIOS : ROM::Name::MasterSystemWesternBIOS;
|
||||
ROM::Request request(bios_name, true);
|
||||
auto roms = rom_fetcher(request);
|
||||
request.validate(roms);
|
||||
|
||||
const auto rom = roms.find(bios_name);
|
||||
if(rom == roms.end()) {
|
||||
// No BIOS found; attempt to boot as though it has already disabled itself.
|
||||
has_bios_ = false;
|
||||
memory_control_ |= 0x08;
|
||||
std::cerr << "No BIOS found; attempting to start cartridge directly" << std::endl;
|
||||
} else {
|
||||
has_bios_ = true;
|
||||
memcpy(&bios_, rom->second.data(), std::min(sizeof(bios_), rom->second.size()));
|
||||
}
|
||||
page_cartridge();
|
||||
|
||||
// Map RAM.
|
||||
if constexpr (is_master_system(model)) {
|
||||
map(read_pointers_, ram_, 8*1024, 0xc000, 0x10000);
|
||||
map(write_pointers_, ram_, 8*1024, 0xc000, 0x10000);
|
||||
} else {
|
||||
map(read_pointers_, ram_, 1024, 0xc000, 0x10000);
|
||||
map(write_pointers_, ram_, 1024, 0xc000, 0x10000);
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
vdp_.last_valid()->set_tv_standard(
|
||||
(region_ == Target::Region::Europe) ?
|
||||
TI::TMS::TVStandard::PAL : TI::TMS::TVStandard::NTSC);
|
||||
// Apply a relatively low low-pass filter. More guidance needed here.
|
||||
// TODO: this is disabled for now since it isn't applicable for the FM chip, I think.
|
||||
// speaker_.set_high_frequency_cutoff(8000);
|
||||
|
||||
// Doing the following would be technically correct, but isn't
|
||||
// especially thread-safe and won't make a substantial difference.
|
||||
// time_until_debounce_ = vdp_->get_time_until_line(-1);
|
||||
// Set default mixer levels: FM off, SN full-throttle.
|
||||
set_mixer_levels(0);
|
||||
|
||||
vdp_.last_valid()->set_scan_target(scan_target);
|
||||
keyboard_.set_delegate(this);
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
ChangeEffect effect_for_file_did_change(const std::string &) const final {
|
||||
return ChangeEffect::RestartMachine;
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
vdp_.last_valid()->set_tv_standard(
|
||||
(region_ == Target::Region::Europe) ?
|
||||
TI::TMS::TVStandard::PAL : TI::TMS::TVStandard::NTSC);
|
||||
|
||||
// Doing the following would be technically correct, but isn't
|
||||
// especially thread-safe and won't make a substantial difference.
|
||||
// time_until_debounce_ = vdp_->get_time_until_line(-1);
|
||||
|
||||
vdp_.last_valid()->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return vdp_.last_valid()->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
vdp_.last_valid()->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType get_display_type() const final {
|
||||
return vdp_.last_valid()->get_display_type();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) final {
|
||||
z80_.run_for(cycles);
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Video) {
|
||||
vdp_.flush();
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return vdp_.last_valid()->get_scaled_scan_status();
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
vdp_.last_valid()->set_display_type(display_type);
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
if(vdp_ += cycle.length) {
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
|
||||
}
|
||||
time_since_sn76489_update_ += cycle.length;
|
||||
|
||||
Outputs::Display::DisplayType get_display_type() const final {
|
||||
return vdp_.last_valid()->get_display_type();
|
||||
}
|
||||
if(cycle.is_terminal()) {
|
||||
uint16_t address = cycle.address ? *cycle.address : 0x0000;
|
||||
switch(cycle.operation) {
|
||||
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
||||
case CPU::Z80::PartialMachineCycle::Read:
|
||||
*cycle.value = read_pointers_[address >> 10] ? read_pointers_[address >> 10][address & 1023] : 0xff;
|
||||
break;
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &speaker_;
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) final {
|
||||
z80_.run_for(cycles);
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Video) {
|
||||
vdp_.flush();
|
||||
}
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
}
|
||||
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
if(vdp_ += cycle.length) {
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
|
||||
}
|
||||
time_since_sn76489_update_ += cycle.length;
|
||||
|
||||
if(cycle.is_terminal()) {
|
||||
uint16_t address = cycle.address ? *cycle.address : 0x0000;
|
||||
switch(cycle.operation) {
|
||||
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
||||
case CPU::Z80::PartialMachineCycle::Read:
|
||||
*cycle.value = read_pointers_[address >> 10] ? read_pointers_[address >> 10][address & 1023] : 0xff;
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Write:
|
||||
if(paging_scheme_ == Target::PagingScheme::Sega) {
|
||||
if(address >= 0xfffd && cartridge_.size() > 48*1024) {
|
||||
if(paging_registers_[address - 0xfffd] != *cycle.value) {
|
||||
paging_registers_[address - 0xfffd] = *cycle.value;
|
||||
page_cartridge();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// i.e. this is the Codemasters paging scheme.
|
||||
if(!(address&0x3fff) && address < 0xc000) {
|
||||
if(paging_registers_[address >> 14] != *cycle.value) {
|
||||
paging_registers_[address >> 14] = *cycle.value;
|
||||
page_cartridge();
|
||||
}
|
||||
case CPU::Z80::PartialMachineCycle::Write:
|
||||
if(paging_scheme_ == Target::PagingScheme::Sega) {
|
||||
if(address >= 0xfffd && cartridge_.size() > 48*1024) {
|
||||
if(paging_registers_[address - 0xfffd] != *cycle.value) {
|
||||
paging_registers_[address - 0xfffd] = *cycle.value;
|
||||
page_cartridge();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// i.e. this is the Codemasters paging scheme.
|
||||
if(!(address&0x3fff) && address < 0xc000) {
|
||||
if(paging_registers_[address >> 14] != *cycle.value) {
|
||||
paging_registers_[address >> 14] = *cycle.value;
|
||||
page_cartridge();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(write_pointers_[address >> 10]) write_pointers_[address >> 10][address & 1023] = *cycle.value;
|
||||
// else logger.info().append("Ignored write to ROM");
|
||||
break;
|
||||
if(write_pointers_[address >> 10]) write_pointers_[address >> 10][address & 1023] = *cycle.value;
|
||||
// else logger.info().append("Ignored write to ROM");
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
switch(address & 0xc1) {
|
||||
case 0x00:
|
||||
logger.error().append("TODO: [input] memory control");
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
case 0x01:
|
||||
logger.error().append("TODO: [input] I/O port control");
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
case 0x40:
|
||||
*cycle.value = vdp_->get_current_line();
|
||||
break;
|
||||
case 0x41:
|
||||
*cycle.value = vdp_.last_valid()->get_latched_horizontal_counter();
|
||||
break;
|
||||
case 0x80: case 0x81:
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
break;
|
||||
case 0xc0: {
|
||||
if(memory_control_ & 0x4) {
|
||||
if(has_fm_audio_ && (address & 0xff) == 0xf2) {
|
||||
*cycle.value = opll_detection_word_;
|
||||
} else {
|
||||
*cycle.value = 0xff;
|
||||
}
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
switch(address & 0xc1) {
|
||||
case 0x00:
|
||||
logger.error().append("TODO: [input] memory control");
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
case 0x01:
|
||||
logger.error().append("TODO: [input] I/O port control");
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
case 0x40:
|
||||
*cycle.value = vdp_->get_current_line();
|
||||
break;
|
||||
case 0x41:
|
||||
*cycle.value = vdp_.last_valid()->get_latched_horizontal_counter();
|
||||
break;
|
||||
case 0x80: case 0x81:
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
break;
|
||||
case 0xc0: {
|
||||
if(memory_control_ & 0x4) {
|
||||
if(has_fm_audio_ && (address & 0xff) == 0xf2) {
|
||||
*cycle.value = opll_detection_word_;
|
||||
} else {
|
||||
Joystick *const joypad1 = static_cast<Joystick *>(joysticks_[0].get());
|
||||
Joystick *const joypad2 = static_cast<Joystick *>(joysticks_[1].get());
|
||||
*cycle.value = uint8_t(joypad1->get_state() | (joypad2->get_state() << 6));
|
||||
}
|
||||
} break;
|
||||
case 0xc1: {
|
||||
if(memory_control_ & 0x4) {
|
||||
*cycle.value = 0xff;
|
||||
} else {
|
||||
Joystick *const joypad2 = static_cast<Joystick *>(joysticks_[1].get());
|
||||
|
||||
*cycle.value =
|
||||
(joypad2->get_state() >> 2) |
|
||||
0x30 |
|
||||
get_th_values();
|
||||
}
|
||||
} break;
|
||||
} else {
|
||||
Joystick *const joypad1 = static_cast<Joystick *>(joysticks_[0].get());
|
||||
Joystick *const joypad2 = static_cast<Joystick *>(joysticks_[1].get());
|
||||
*cycle.value = uint8_t(joypad1->get_state() | (joypad2->get_state() << 6));
|
||||
}
|
||||
} break;
|
||||
case 0xc1: {
|
||||
if(memory_control_ & 0x4) {
|
||||
*cycle.value = 0xff;
|
||||
} else {
|
||||
Joystick *const joypad2 = static_cast<Joystick *>(joysticks_[1].get());
|
||||
|
||||
default:
|
||||
logger.error().append("[input] Clearly some sort of typo");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
*cycle.value =
|
||||
(joypad2->get_state() >> 2) |
|
||||
0x30 |
|
||||
get_th_values();
|
||||
}
|
||||
} break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Output:
|
||||
switch(address & 0xc1) {
|
||||
case 0x00: // i.e. even ports less than 0x40.
|
||||
if constexpr (is_master_system(model)) {
|
||||
// TODO: Obey the RAM enable.
|
||||
logger.info().append("Memory control: %02x", memory_control_);
|
||||
memory_control_ = *cycle.value;
|
||||
page_cartridge();
|
||||
default:
|
||||
logger.error().append("[input] Clearly some sort of typo");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Output:
|
||||
switch(address & 0xc1) {
|
||||
case 0x00: // i.e. even ports less than 0x40.
|
||||
if constexpr (is_master_system(model)) {
|
||||
// TODO: Obey the RAM enable.
|
||||
logger.info().append("Memory control: %02x", memory_control_);
|
||||
memory_control_ = *cycle.value;
|
||||
page_cartridge();
|
||||
}
|
||||
break;
|
||||
case 0x01: { // i.e. odd ports less than 0x40.
|
||||
// A programmer can force the TH lines to 0 here,
|
||||
// causing a phoney lightgun latch, so check for any
|
||||
// discontinuity in TH inputs.
|
||||
const auto previous_ths = get_th_values();
|
||||
io_port_control_ = *cycle.value;
|
||||
const auto new_ths = get_th_values();
|
||||
|
||||
// Latch if either TH has newly gone to 1.
|
||||
if((new_ths^previous_ths)&new_ths) {
|
||||
vdp_->latch_horizontal_counter();
|
||||
}
|
||||
} break;
|
||||
case 0x40: case 0x41: // i.e. ports 0x40–0x7f.
|
||||
update_audio();
|
||||
sn76489_.write(*cycle.value);
|
||||
break;
|
||||
case 0x80: case 0x81: // i.e. ports 0x80–0xbf.
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
break;
|
||||
case 0xc1: case 0xc0: // i.e. ports 0xc0–0xff.
|
||||
if(has_fm_audio_) {
|
||||
switch(address & 0xff) {
|
||||
case 0xf0: case 0xf1:
|
||||
update_audio();
|
||||
opll_.write(address, *cycle.value);
|
||||
break;
|
||||
case 0xf2:
|
||||
opll_detection_word_ = *cycle.value;
|
||||
set_mixer_levels(opll_detection_word_);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x01: { // i.e. odd ports less than 0x40.
|
||||
// A programmer can force the TH lines to 0 here,
|
||||
// causing a phoney lightgun latch, so check for any
|
||||
// discontinuity in TH inputs.
|
||||
const auto previous_ths = get_th_values();
|
||||
io_port_control_ = *cycle.value;
|
||||
const auto new_ths = get_th_values();
|
||||
}
|
||||
break;
|
||||
|
||||
// Latch if either TH has newly gone to 1.
|
||||
if((new_ths^previous_ths)&new_ths) {
|
||||
vdp_->latch_horizontal_counter();
|
||||
}
|
||||
} break;
|
||||
case 0x40: case 0x41: // i.e. ports 0x40–0x7f.
|
||||
update_audio();
|
||||
sn76489_.write(*cycle.value);
|
||||
break;
|
||||
case 0x80: case 0x81: // i.e. ports 0x80–0xbf.
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
break;
|
||||
case 0xc1: case 0xc0: // i.e. ports 0xc0–0xff.
|
||||
if(has_fm_audio_) {
|
||||
switch(address & 0xff) {
|
||||
case 0xf0: case 0xf1:
|
||||
update_audio();
|
||||
opll_.write(address, *cycle.value);
|
||||
break;
|
||||
case 0xf2:
|
||||
opll_detection_word_ = *cycle.value;
|
||||
set_mixer_levels(opll_detection_word_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.error().append("[output] Clearly some sort of typo");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.error().append("[output] Clearly some sort of typo");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
/*
|
||||
TODO: implementation of the below is incomplete.
|
||||
@@ -380,181 +380,181 @@ template <Analyser::Static::Sega::Target::Model model> class ConcreteMachine:
|
||||
— Charles MacDonald
|
||||
*/
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Interrupt:
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
case CPU::Z80::PartialMachineCycle::Interrupt:
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// The pause button is debounced and takes effect only one line before pixels
|
||||
// begin; time_until_debounce_ keeps track of the time until then.
|
||||
time_until_debounce_ -= cycle.length;
|
||||
if(time_until_debounce_ <= HalfCycles(0)) {
|
||||
z80_.set_non_maskable_interrupt_line(pause_is_pressed_);
|
||||
time_until_debounce_ = vdp_->get_time_until_line(-1);
|
||||
}
|
||||
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
// MARK: - Keyboard (i.e. the pause and reset buttons).
|
||||
Inputs::Keyboard &get_keyboard() final {
|
||||
return keyboard_;
|
||||
}
|
||||
|
||||
bool keyboard_did_change_key(Inputs::Keyboard &, Inputs::Keyboard::Key key, bool is_pressed) final {
|
||||
if(key == Inputs::Keyboard::Key::Enter) {
|
||||
pause_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
}
|
||||
|
||||
if(key == Inputs::Keyboard::Key::Escape) {
|
||||
reset_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void reset_all_keys(Inputs::Keyboard &) final {
|
||||
}
|
||||
|
||||
// MARK: - Configuration options.
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->output = get_video_signal_configurable();
|
||||
return options;
|
||||
}
|
||||
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
set_video_signal_configurable(options->output);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr TI::TMS::Personality tms_personality() {
|
||||
switch(model) {
|
||||
default:
|
||||
case Target::Model::SG1000: return TI::TMS::TMS9918A;
|
||||
case Target::Model::MasterSystem: return TI::TMS::SMSVDP;
|
||||
case Target::Model::MasterSystem2: return TI::TMS::SMS2VDP;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
inline uint8_t get_th_values() {
|
||||
// Quick not on TH inputs here: if either is setup as an output, then the
|
||||
// currently output level is returned. Otherwise they're fixed at 1.
|
||||
return
|
||||
uint8_t(
|
||||
((io_port_control_ & 0x02) << 5) | ((io_port_control_&0x20) << 1) |
|
||||
((io_port_control_ & 0x08) << 4) | (io_port_control_&0x80)
|
||||
);
|
||||
|
||||
// The pause button is debounced and takes effect only one line before pixels
|
||||
// begin; time_until_debounce_ keeps track of the time until then.
|
||||
time_until_debounce_ -= cycle.length;
|
||||
if(time_until_debounce_ <= HalfCycles(0)) {
|
||||
z80_.set_non_maskable_interrupt_line(pause_is_pressed_);
|
||||
time_until_debounce_ = vdp_->get_time_until_line(-1);
|
||||
}
|
||||
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(audio_divider)));
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
// MARK: - Keyboard (i.e. the pause and reset buttons).
|
||||
Inputs::Keyboard &get_keyboard() final {
|
||||
return keyboard_;
|
||||
}
|
||||
|
||||
bool keyboard_did_change_key(Inputs::Keyboard &, Inputs::Keyboard::Key key, bool is_pressed) final {
|
||||
if(key == Inputs::Keyboard::Key::Enter) {
|
||||
pause_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
}
|
||||
|
||||
void set_mixer_levels(uint8_t mode) {
|
||||
// This is as per the audio control register;
|
||||
// see https://www.smspower.org/Development/AudioControlPort
|
||||
update_audio();
|
||||
audio_queue_.enqueue([this, mode] {
|
||||
switch(mode & 3) {
|
||||
case 0: // SN76489 only; the default.
|
||||
mixer_.set_relative_volumes({1.0f, 0.0f});
|
||||
break;
|
||||
|
||||
case 1: // FM only.
|
||||
mixer_.set_relative_volumes({0.0f, 1.0f});
|
||||
break;
|
||||
|
||||
case 2: // No audio.
|
||||
mixer_.set_relative_volumes({0.0f, 0.0f});
|
||||
break;
|
||||
|
||||
case 3: // Both FM and SN76489.
|
||||
mixer_.set_relative_volumes({0.5f, 0.5f});
|
||||
break;
|
||||
}
|
||||
});
|
||||
if(key == Inputs::Keyboard::Key::Escape) {
|
||||
reset_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
}
|
||||
|
||||
using Target = Analyser::Static::Sega::Target;
|
||||
const Target::Region region_;
|
||||
const Target::PagingScheme paging_scheme_;
|
||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||
JustInTimeActor<TI::TMS::TMS9918<tms_personality()>> vdp_;
|
||||
return false;
|
||||
}
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
Yamaha::OPL::OPLL opll_;
|
||||
Outputs::Speaker::CompoundSource<decltype(sn76489_), decltype(opll_)> mixer_;
|
||||
Outputs::Speaker::PullLowpass<decltype(mixer_)> speaker_;
|
||||
uint8_t opll_detection_word_ = 0xff;
|
||||
void reset_all_keys(Inputs::Keyboard &) final {
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
Inputs::Keyboard keyboard_;
|
||||
bool reset_is_pressed_ = false, pause_is_pressed_ = false;
|
||||
// MARK: - Configuration options.
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||
options->output = get_video_signal_configurable();
|
||||
return options;
|
||||
}
|
||||
|
||||
HalfCycles time_since_sn76489_update_;
|
||||
HalfCycles time_until_debounce_;
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
set_video_signal_configurable(options->output);
|
||||
}
|
||||
|
||||
uint8_t ram_[8*1024];
|
||||
uint8_t bios_[8*1024];
|
||||
std::vector<uint8_t> cartridge_;
|
||||
private:
|
||||
static constexpr TI::TMS::Personality tms_personality() {
|
||||
switch(model) {
|
||||
default:
|
||||
case Target::Model::SG1000: return TI::TMS::TMS9918A;
|
||||
case Target::Model::MasterSystem: return TI::TMS::SMSVDP;
|
||||
case Target::Model::MasterSystem2: return TI::TMS::SMS2VDP;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t io_port_control_ = 0x0f;
|
||||
inline uint8_t get_th_values() {
|
||||
// Quick not on TH inputs here: if either is setup as an output, then the
|
||||
// currently output level is returned. Otherwise they're fixed at 1.
|
||||
return
|
||||
uint8_t(
|
||||
((io_port_control_ & 0x02) << 5) | ((io_port_control_&0x20) << 1) |
|
||||
((io_port_control_ & 0x08) << 4) | (io_port_control_&0x80)
|
||||
);
|
||||
|
||||
// This is a static constexpr for now; I may use it in the future.
|
||||
static constexpr bool has_fm_audio_ = true;
|
||||
}
|
||||
|
||||
// The memory map has a 1kb granularity; this is determined by the SG1000's 1kb of RAM.
|
||||
const uint8_t *read_pointers_[64];
|
||||
uint8_t *write_pointers_[64];
|
||||
template <typename T> void map(T **target, uint8_t *source, size_t size, size_t start_address, size_t end_address = 0) {
|
||||
if(!end_address) end_address = start_address + size;
|
||||
for(auto address = start_address; address < end_address; address += 1024) {
|
||||
target[address >> 10] = source ? &source[(address - start_address) & (size - 1)] : nullptr;
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(audio_divider)));
|
||||
}
|
||||
|
||||
void set_mixer_levels(uint8_t mode) {
|
||||
// This is as per the audio control register;
|
||||
// see https://www.smspower.org/Development/AudioControlPort
|
||||
update_audio();
|
||||
audio_queue_.enqueue([this, mode] {
|
||||
switch(mode & 3) {
|
||||
case 0: // SN76489 only; the default.
|
||||
mixer_.set_relative_volumes({1.0f, 0.0f});
|
||||
break;
|
||||
|
||||
case 1: // FM only.
|
||||
mixer_.set_relative_volumes({0.0f, 1.0f});
|
||||
break;
|
||||
|
||||
case 2: // No audio.
|
||||
mixer_.set_relative_volumes({0.0f, 0.0f});
|
||||
break;
|
||||
|
||||
case 3: // Both FM and SN76489.
|
||||
mixer_.set_relative_volumes({0.5f, 0.5f});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
using Target = Analyser::Static::Sega::Target;
|
||||
const Target::Region region_;
|
||||
const Target::PagingScheme paging_scheme_;
|
||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||
JustInTimeActor<TI::TMS::TMS9918<tms_personality()>> vdp_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
Yamaha::OPL::OPLL opll_;
|
||||
Outputs::Speaker::CompoundSource<decltype(sn76489_), decltype(opll_)> mixer_;
|
||||
Outputs::Speaker::PullLowpass<decltype(mixer_)> speaker_;
|
||||
uint8_t opll_detection_word_ = 0xff;
|
||||
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
Inputs::Keyboard keyboard_;
|
||||
bool reset_is_pressed_ = false, pause_is_pressed_ = false;
|
||||
|
||||
HalfCycles time_since_sn76489_update_;
|
||||
HalfCycles time_until_debounce_;
|
||||
|
||||
uint8_t ram_[8*1024];
|
||||
uint8_t bios_[8*1024];
|
||||
std::vector<uint8_t> cartridge_;
|
||||
|
||||
uint8_t io_port_control_ = 0x0f;
|
||||
|
||||
// This is a static constexpr for now; I may use it in the future.
|
||||
static constexpr bool has_fm_audio_ = true;
|
||||
|
||||
// The memory map has a 1kb granularity; this is determined by the SG1000's 1kb of RAM.
|
||||
const uint8_t *read_pointers_[64];
|
||||
uint8_t *write_pointers_[64];
|
||||
template <typename T> void map(T **target, uint8_t *source, size_t size, size_t start_address, size_t end_address = 0) {
|
||||
if(!end_address) end_address = start_address + size;
|
||||
for(auto address = start_address; address < end_address; address += 1024) {
|
||||
target[address >> 10] = source ? &source[(address - start_address) & (size - 1)] : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t paging_registers_[3] = {0, 1, 2};
|
||||
uint8_t memory_control_ = 0;
|
||||
void page_cartridge() {
|
||||
// Either install the cartridge or don't; Japanese machines can't see
|
||||
// anything but the cartridge.
|
||||
if(!(memory_control_ & 0x40) || region_ == Target::Region::Japan) {
|
||||
for(size_t c = 0; c < 3; ++c) {
|
||||
const size_t start_addr = (paging_registers_[c] * 0x4000) % cartridge_.size();
|
||||
map(
|
||||
read_pointers_,
|
||||
cartridge_.data() + start_addr,
|
||||
std::min(size_t(0x4000), cartridge_.size() - start_addr),
|
||||
c * 0x4000);
|
||||
}
|
||||
|
||||
// The first 1kb doesn't page though, if this is the Sega paging scheme.
|
||||
if(paging_scheme_ == Target::PagingScheme::Sega) {
|
||||
map(read_pointers_, cartridge_.data(), 0x400, 0x0000);
|
||||
}
|
||||
} else {
|
||||
map(read_pointers_, nullptr, 0xc000, 0x0000);
|
||||
uint8_t paging_registers_[3] = {0, 1, 2};
|
||||
uint8_t memory_control_ = 0;
|
||||
void page_cartridge() {
|
||||
// Either install the cartridge or don't; Japanese machines can't see
|
||||
// anything but the cartridge.
|
||||
if(!(memory_control_ & 0x40) || region_ == Target::Region::Japan) {
|
||||
for(size_t c = 0; c < 3; ++c) {
|
||||
const size_t start_addr = (paging_registers_[c] * 0x4000) % cartridge_.size();
|
||||
map(
|
||||
read_pointers_,
|
||||
cartridge_.data() + start_addr,
|
||||
std::min(size_t(0x4000), cartridge_.size() - start_addr),
|
||||
c * 0x4000);
|
||||
}
|
||||
|
||||
// Throw the BIOS on top if this machine has one and it isn't disabled.
|
||||
if(has_bios_ && !(memory_control_ & 0x08)) {
|
||||
map(read_pointers_, bios_, 8*1024, 0);
|
||||
// The first 1kb doesn't page though, if this is the Sega paging scheme.
|
||||
if(paging_scheme_ == Target::PagingScheme::Sega) {
|
||||
map(read_pointers_, cartridge_.data(), 0x400, 0x0000);
|
||||
}
|
||||
} else {
|
||||
map(read_pointers_, nullptr, 0xc000, 0x0000);
|
||||
}
|
||||
bool has_bios_ = true;
|
||||
|
||||
// Throw the BIOS on top if this machine has one and it isn't disabled.
|
||||
if(has_bios_ && !(memory_control_ & 0x08)) {
|
||||
map(read_pointers_, bios_, 8*1024, 0);
|
||||
}
|
||||
}
|
||||
bool has_bios_ = true;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -23,33 +23,33 @@ namespace Oric {
|
||||
see the thread at https://forum.defence-force.org/viewtopic.php?f=25&t=2055
|
||||
*/
|
||||
class BD500: public DiskController {
|
||||
public:
|
||||
BD500();
|
||||
public:
|
||||
BD500();
|
||||
|
||||
void write(int address, uint8_t value);
|
||||
uint8_t read(int address);
|
||||
void write(int address, uint8_t value);
|
||||
uint8_t read(int address);
|
||||
|
||||
void run_for(const Cycles cycles);
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
|
||||
private:
|
||||
void set_head_load_request(bool head_load) final;
|
||||
bool is_loading_head_ = false;
|
||||
Activity::Observer *observer_ = nullptr;
|
||||
private:
|
||||
void set_head_load_request(bool head_load) final;
|
||||
bool is_loading_head_ = false;
|
||||
Activity::Observer *observer_ = nullptr;
|
||||
|
||||
void access(int address);
|
||||
void set_head_loaded(bool loaded);
|
||||
void access(int address);
|
||||
void set_head_loaded(bool loaded);
|
||||
|
||||
bool enable_overlay_ram_ = false;
|
||||
bool disable_basic_rom_ = false;
|
||||
void select_paged_item() {
|
||||
PagedItem item = PagedItem::RAM;
|
||||
if(!enable_overlay_ram_) {
|
||||
item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC;
|
||||
}
|
||||
set_paged_item(item);
|
||||
bool enable_overlay_ram_ = false;
|
||||
bool disable_basic_rom_ = false;
|
||||
void select_paged_item() {
|
||||
PagedItem item = PagedItem::RAM;
|
||||
if(!enable_overlay_ram_) {
|
||||
item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC;
|
||||
}
|
||||
set_paged_item(item);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -11,50 +11,50 @@
|
||||
namespace Oric {
|
||||
|
||||
class DiskController: public WD::WD1770 {
|
||||
public:
|
||||
DiskController(WD::WD1770::Personality personality, int clock_rate, Storage::Disk::Drive::ReadyType ready_type) :
|
||||
WD::WD1770(personality), clock_rate_(clock_rate), ready_type_(ready_type) {
|
||||
emplace_drives(4, clock_rate_, 300, 2, ready_type_);
|
||||
// TODO: don't assume four drives?
|
||||
public:
|
||||
DiskController(WD::WD1770::Personality personality, int clock_rate, Storage::Disk::Drive::ReadyType ready_type) :
|
||||
WD::WD1770(personality), clock_rate_(clock_rate), ready_type_(ready_type) {
|
||||
emplace_drives(4, clock_rate_, 300, 2, ready_type_);
|
||||
// TODO: don't assume four drives?
|
||||
}
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int d) {
|
||||
get_drive(size_t(d)).set_disk(disk);
|
||||
}
|
||||
|
||||
enum class PagedItem {
|
||||
DiskROM,
|
||||
BASIC,
|
||||
RAM
|
||||
};
|
||||
|
||||
struct Delegate: public WD1770::Delegate {
|
||||
virtual void disk_controller_did_change_paged_item(DiskController &) = 0;
|
||||
};
|
||||
inline void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
WD1770::set_delegate(delegate);
|
||||
if(delegate) delegate->disk_controller_did_change_paged_item(*this);
|
||||
}
|
||||
inline PagedItem get_paged_item() {
|
||||
return paged_item_;
|
||||
}
|
||||
|
||||
protected:
|
||||
Delegate *delegate_ = nullptr;
|
||||
|
||||
void set_paged_item(PagedItem item) {
|
||||
if(paged_item_ == item) return;
|
||||
paged_item_ = item;
|
||||
if(delegate_) {
|
||||
delegate_->disk_controller_did_change_paged_item(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int d) {
|
||||
get_drive(size_t(d)).set_disk(disk);
|
||||
}
|
||||
|
||||
enum class PagedItem {
|
||||
DiskROM,
|
||||
BASIC,
|
||||
RAM
|
||||
};
|
||||
|
||||
struct Delegate: public WD1770::Delegate {
|
||||
virtual void disk_controller_did_change_paged_item(DiskController &) = 0;
|
||||
};
|
||||
inline void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
WD1770::set_delegate(delegate);
|
||||
if(delegate) delegate->disk_controller_did_change_paged_item(*this);
|
||||
}
|
||||
inline PagedItem get_paged_item() {
|
||||
return paged_item_;
|
||||
}
|
||||
|
||||
protected:
|
||||
Delegate *delegate_ = nullptr;
|
||||
|
||||
void set_paged_item(PagedItem item) {
|
||||
if(paged_item_ == item) return;
|
||||
paged_item_ = item;
|
||||
if(delegate_) {
|
||||
delegate_->disk_controller_did_change_paged_item(*this);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
PagedItem paged_item_ = PagedItem::DiskROM;
|
||||
int clock_rate_;
|
||||
Storage::Disk::Drive::ReadyType ready_type_;
|
||||
private:
|
||||
PagedItem paged_item_ = PagedItem::DiskROM;
|
||||
int clock_rate_;
|
||||
Storage::Disk::Drive::ReadyType ready_type_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -15,29 +15,29 @@
|
||||
namespace Oric {
|
||||
|
||||
class Jasmin: public DiskController {
|
||||
public:
|
||||
Jasmin();
|
||||
public:
|
||||
Jasmin();
|
||||
|
||||
void write(int address, uint8_t value);
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
|
||||
private:
|
||||
void set_motor_on(bool on) final;
|
||||
bool motor_on_ = false;
|
||||
uint8_t selected_drives_ = 0;
|
||||
private:
|
||||
void set_motor_on(bool on) final;
|
||||
bool motor_on_ = false;
|
||||
uint8_t selected_drives_ = 0;
|
||||
|
||||
Activity::Observer *observer_ = nullptr;
|
||||
Activity::Observer *observer_ = nullptr;
|
||||
|
||||
bool enable_overlay_ram_ = false;
|
||||
bool disable_basic_rom_ = false;
|
||||
void select_paged_item() {
|
||||
PagedItem item = PagedItem::RAM;
|
||||
if(!enable_overlay_ram_) {
|
||||
item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC;
|
||||
}
|
||||
set_paged_item(item);
|
||||
bool enable_overlay_ram_ = false;
|
||||
bool disable_basic_rom_ = false;
|
||||
void select_paged_item() {
|
||||
PagedItem item = PagedItem::RAM;
|
||||
if(!enable_overlay_ram_) {
|
||||
item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC;
|
||||
}
|
||||
set_paged_item(item);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -15,30 +15,30 @@
|
||||
namespace Oric {
|
||||
|
||||
class Microdisc: public DiskController {
|
||||
public:
|
||||
Microdisc();
|
||||
public:
|
||||
Microdisc();
|
||||
|
||||
void set_control_register(uint8_t control);
|
||||
uint8_t get_interrupt_request_register();
|
||||
uint8_t get_data_request_register();
|
||||
void set_control_register(uint8_t control);
|
||||
uint8_t get_interrupt_request_register();
|
||||
uint8_t get_data_request_register();
|
||||
|
||||
bool get_interrupt_request_line();
|
||||
bool get_interrupt_request_line();
|
||||
|
||||
void run_for(const Cycles cycles);
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
|
||||
private:
|
||||
void set_head_load_request(bool head_load) final;
|
||||
private:
|
||||
void set_head_load_request(bool head_load) final;
|
||||
|
||||
void set_control_register(uint8_t control, uint8_t changes);
|
||||
uint8_t last_control_ = 0;
|
||||
bool irq_enable_ = false;
|
||||
void set_control_register(uint8_t control, uint8_t changes);
|
||||
uint8_t last_control_ = 0;
|
||||
bool irq_enable_ = false;
|
||||
|
||||
Cycles::IntType head_load_request_counter_ = -1;
|
||||
bool head_load_request_ = false;
|
||||
Cycles::IntType head_load_request_counter_ = -1;
|
||||
bool head_load_request_ = false;
|
||||
|
||||
Activity::Observer *observer_ = nullptr;
|
||||
Activity::Observer *observer_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -18,52 +18,52 @@
|
||||
namespace Oric {
|
||||
|
||||
class VideoOutput {
|
||||
public:
|
||||
VideoOutput(uint8_t *memory);
|
||||
void set_colour_rom(const std::vector<uint8_t> &colour_rom);
|
||||
public:
|
||||
VideoOutput(uint8_t *memory);
|
||||
void set_colour_rom(const std::vector<uint8_t> &colour_rom);
|
||||
|
||||
void run_for(const Cycles cycles);
|
||||
Cycles next_sequence_point() const;
|
||||
void run_for(const Cycles cycles);
|
||||
Cycles next_sequence_point() const;
|
||||
|
||||
bool vsync();
|
||||
bool vsync();
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
void set_display_type(Outputs::Display::DisplayType display_type);
|
||||
Outputs::Display::DisplayType get_display_type() const;
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
void set_display_type(Outputs::Display::DisplayType display_type);
|
||||
Outputs::Display::DisplayType get_display_type() const;
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
void register_crt_frequency_mismatch();
|
||||
void register_crt_frequency_mismatch();
|
||||
|
||||
private:
|
||||
uint8_t *ram_;
|
||||
Outputs::CRT::CRT crt_;
|
||||
Outputs::CRT::CRTFrequencyMismatchWarner<VideoOutput> frequency_mismatch_warner_;
|
||||
bool crt_is_60Hz_ = false;
|
||||
bool has_colour_rom_ = false;
|
||||
private:
|
||||
uint8_t *ram_;
|
||||
Outputs::CRT::CRT crt_;
|
||||
Outputs::CRT::CRTFrequencyMismatchWarner<VideoOutput> frequency_mismatch_warner_;
|
||||
bool crt_is_60Hz_ = false;
|
||||
bool has_colour_rom_ = false;
|
||||
|
||||
void update_crt_frequency();
|
||||
void update_crt_frequency();
|
||||
|
||||
// Counters and limits.
|
||||
int counter_ = 0, frame_counter_ = 0;
|
||||
int v_sync_start_position_, v_sync_end_position_, counter_period_;
|
||||
// Counters and limits.
|
||||
int counter_ = 0, frame_counter_ = 0;
|
||||
int v_sync_start_position_, v_sync_end_position_, counter_period_;
|
||||
|
||||
// Output target and device.
|
||||
uint8_t *rgb_pixel_target_ = nullptr;
|
||||
uint32_t *composite_pixel_target_ = nullptr;
|
||||
uint32_t colour_forms_[8];
|
||||
Outputs::Display::InputDataType data_type_;
|
||||
// Output target and device.
|
||||
uint8_t *rgb_pixel_target_ = nullptr;
|
||||
uint32_t *composite_pixel_target_ = nullptr;
|
||||
uint32_t colour_forms_[8];
|
||||
Outputs::Display::InputDataType data_type_;
|
||||
|
||||
// Registers.
|
||||
uint8_t ink_, paper_;
|
||||
// Registers.
|
||||
uint8_t ink_, paper_;
|
||||
|
||||
int character_set_base_address_ = 0xb400;
|
||||
inline void set_character_set_base_address();
|
||||
int character_set_base_address_ = 0xb400;
|
||||
inline void set_character_set_base_address();
|
||||
|
||||
bool is_graphics_mode_ = false;
|
||||
bool next_frame_is_sixty_hertz_ = false;
|
||||
bool use_alternative_character_set_;
|
||||
bool use_double_height_characters_;
|
||||
bool blink_text_;
|
||||
bool is_graphics_mode_ = false;
|
||||
bool next_frame_is_sixty_hertz_ = false;
|
||||
bool use_alternative_character_set_;
|
||||
bool use_double_height_characters_;
|
||||
bool blink_text_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -37,38 +37,38 @@ enum Key: uint16_t {
|
||||
};
|
||||
|
||||
class Keyboard {
|
||||
public:
|
||||
Keyboard(Machine machine);
|
||||
public:
|
||||
Keyboard(Machine machine);
|
||||
|
||||
void set_key_state(uint16_t key, bool is_pressed);
|
||||
void clear_all_keys();
|
||||
void set_key_state(uint16_t key, bool is_pressed);
|
||||
void clear_all_keys();
|
||||
|
||||
uint8_t read(uint16_t address);
|
||||
uint8_t read(uint16_t address);
|
||||
|
||||
private:
|
||||
uint8_t key_states_[8];
|
||||
const Machine machine_;
|
||||
private:
|
||||
uint8_t key_states_[8];
|
||||
const Machine machine_;
|
||||
};
|
||||
|
||||
class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
public:
|
||||
KeyboardMapper(Machine machine);
|
||||
public:
|
||||
KeyboardMapper(Machine machine);
|
||||
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const override;
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const override;
|
||||
|
||||
private:
|
||||
const Machine machine_;
|
||||
private:
|
||||
const Machine machine_;
|
||||
};
|
||||
|
||||
class CharacterMapper: public ::Utility::CharacterMapper {
|
||||
public:
|
||||
CharacterMapper(Machine machine);
|
||||
const uint16_t *sequence_for_character(char character) const override;
|
||||
public:
|
||||
CharacterMapper(Machine machine);
|
||||
const uint16_t *sequence_for_character(char character) const override;
|
||||
|
||||
bool needs_pause_after_key(uint16_t key) const override;
|
||||
bool needs_pause_after_key(uint16_t key) const override;
|
||||
|
||||
private:
|
||||
const Machine machine_;
|
||||
private:
|
||||
const Machine machine_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -61,433 +61,433 @@ template<bool is_zx81> class ConcreteMachine:
|
||||
public Utility::TypeRecipient<CharacterMapper>,
|
||||
public CPU::Z80::BusHandler,
|
||||
public Machine {
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::ZX8081::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
Utility::TypeRecipient<CharacterMapper>(keyboard_machine()),
|
||||
z80_(*this),
|
||||
keyboard_(keyboard_machine()),
|
||||
keyboard_mapper_(keyboard_machine()),
|
||||
tape_player_(ZX8081ClockRate),
|
||||
ay_(GI::AY38910::Personality::AY38910, audio_queue_),
|
||||
speaker_(ay_) {
|
||||
set_clock_rate(ZX8081ClockRate);
|
||||
speaker_.set_input_rate(float(ZX8081ClockRate) / 2.0f);
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::ZX8081::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
Utility::TypeRecipient<CharacterMapper>(keyboard_machine()),
|
||||
z80_(*this),
|
||||
keyboard_(keyboard_machine()),
|
||||
keyboard_mapper_(keyboard_machine()),
|
||||
tape_player_(ZX8081ClockRate),
|
||||
ay_(GI::AY38910::Personality::AY38910, audio_queue_),
|
||||
speaker_(ay_) {
|
||||
set_clock_rate(ZX8081ClockRate);
|
||||
speaker_.set_input_rate(float(ZX8081ClockRate) / 2.0f);
|
||||
|
||||
const bool use_zx81_rom = target.is_ZX81 || target.ZX80_uses_ZX81_ROM;
|
||||
const ROM::Name rom_name = use_zx81_rom ? ROM::Name::ZX81 : ROM::Name::ZX80;
|
||||
const ROM::Request request(rom_name);
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
rom_ = std::move(roms.find(rom_name)->second);
|
||||
const bool use_zx81_rom = target.is_ZX81 || target.ZX80_uses_ZX81_ROM;
|
||||
const ROM::Name rom_name = use_zx81_rom ? ROM::Name::ZX81 : ROM::Name::ZX80;
|
||||
const ROM::Request request(rom_name);
|
||||
auto roms = rom_fetcher(request);
|
||||
if(!request.validate(roms)) {
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
rom_ = std::move(roms.find(rom_name)->second);
|
||||
|
||||
rom_mask_ = uint16_t(rom_.size() - 1);
|
||||
rom_mask_ = uint16_t(rom_.size() - 1);
|
||||
|
||||
switch(target.memory_model) {
|
||||
case Analyser::Static::ZX8081::Target::MemoryModel::Unexpanded:
|
||||
ram_.resize(1024);
|
||||
ram_base_ = 16384;
|
||||
ram_mask_ = 1023;
|
||||
break;
|
||||
case Analyser::Static::ZX8081::Target::MemoryModel::SixteenKB:
|
||||
ram_.resize(16384);
|
||||
ram_base_ = 16384;
|
||||
ram_mask_ = 16383;
|
||||
break;
|
||||
case Analyser::Static::ZX8081::Target::MemoryModel::SixtyFourKB:
|
||||
ram_.resize(65536);
|
||||
ram_base_ = 8192;
|
||||
ram_mask_ = 65535;
|
||||
break;
|
||||
}
|
||||
Memory::Fuzz(ram_);
|
||||
switch(target.memory_model) {
|
||||
case Analyser::Static::ZX8081::Target::MemoryModel::Unexpanded:
|
||||
ram_.resize(1024);
|
||||
ram_base_ = 16384;
|
||||
ram_mask_ = 1023;
|
||||
break;
|
||||
case Analyser::Static::ZX8081::Target::MemoryModel::SixteenKB:
|
||||
ram_.resize(16384);
|
||||
ram_base_ = 16384;
|
||||
ram_mask_ = 16383;
|
||||
break;
|
||||
case Analyser::Static::ZX8081::Target::MemoryModel::SixtyFourKB:
|
||||
ram_.resize(65536);
|
||||
ram_base_ = 8192;
|
||||
ram_mask_ = 65535;
|
||||
break;
|
||||
}
|
||||
Memory::Fuzz(ram_);
|
||||
|
||||
// Ensure valid initial key state.
|
||||
clear_all_keys();
|
||||
if(!target.loading_command.empty()) {
|
||||
type_string(target.loading_command);
|
||||
should_autorun_ = true;
|
||||
}
|
||||
|
||||
insert_media(target.media);
|
||||
// Ensure valid initial key state.
|
||||
clear_all_keys();
|
||||
if(!target.loading_command.empty()) {
|
||||
type_string(target.loading_command);
|
||||
should_autorun_ = true;
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
insert_media(target.media);
|
||||
}
|
||||
|
||||
~ConcreteMachine() {
|
||||
audio_queue_.flush();
|
||||
}
|
||||
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
const HalfCycles previous_counter = horizontal_counter_;
|
||||
horizontal_counter_ += cycle.length;
|
||||
time_since_ay_update_ += cycle.length;
|
||||
|
||||
if(previous_counter < vsync_start_ && horizontal_counter_ >= vsync_start_) {
|
||||
video_.run_for(vsync_start_ - previous_counter);
|
||||
set_hsync(true);
|
||||
line_counter_ = (line_counter_ + 1) & 7;
|
||||
if(nmi_is_enabled_) {
|
||||
z80_.set_non_maskable_interrupt_line(true);
|
||||
}
|
||||
video_.run_for(horizontal_counter_ - vsync_start_);
|
||||
} else if(previous_counter < vsync_end_ && horizontal_counter_ >= vsync_end_) {
|
||||
video_.run_for(vsync_end_ - previous_counter);
|
||||
set_hsync(false);
|
||||
if(nmi_is_enabled_) {
|
||||
z80_.set_non_maskable_interrupt_line(false);
|
||||
z80_.set_wait_line(false);
|
||||
}
|
||||
video_.run_for(horizontal_counter_ - vsync_end_);
|
||||
} else {
|
||||
video_.run_for(cycle.length);
|
||||
}
|
||||
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
const HalfCycles previous_counter = horizontal_counter_;
|
||||
horizontal_counter_ += cycle.length;
|
||||
time_since_ay_update_ += cycle.length;
|
||||
if constexpr (is_zx81) horizontal_counter_ %= HalfCycles(Cycles(207));
|
||||
if(!tape_advance_delay_) {
|
||||
tape_player_.run_for(cycle.length);
|
||||
} else {
|
||||
tape_advance_delay_ = std::max(tape_advance_delay_ - cycle.length, HalfCycles(0));
|
||||
}
|
||||
|
||||
if(previous_counter < vsync_start_ && horizontal_counter_ >= vsync_start_) {
|
||||
video_.run_for(vsync_start_ - previous_counter);
|
||||
set_hsync(true);
|
||||
line_counter_ = (line_counter_ + 1) & 7;
|
||||
if(nmi_is_enabled_) {
|
||||
z80_.set_non_maskable_interrupt_line(true);
|
||||
if(nmi_is_enabled_ && !z80_.get_halt_line() && z80_.get_non_maskable_interrupt_line()) {
|
||||
z80_.set_wait_line(true);
|
||||
}
|
||||
|
||||
if(!cycle.is_terminal()) {
|
||||
return Cycles(0);
|
||||
}
|
||||
|
||||
const uint16_t address = cycle.address ? *cycle.address : 0;
|
||||
bool is_opcode_read = false;
|
||||
switch(cycle.operation) {
|
||||
case CPU::Z80::PartialMachineCycle::Output:
|
||||
if(!nmi_is_enabled_) {
|
||||
line_counter_ = 0;
|
||||
set_vsync(false);
|
||||
}
|
||||
video_.run_for(horizontal_counter_ - vsync_start_);
|
||||
} else if(previous_counter < vsync_end_ && horizontal_counter_ >= vsync_end_) {
|
||||
video_.run_for(vsync_end_ - previous_counter);
|
||||
set_hsync(false);
|
||||
if(nmi_is_enabled_) {
|
||||
z80_.set_non_maskable_interrupt_line(false);
|
||||
z80_.set_wait_line(false);
|
||||
if(!(address & 2)) nmi_is_enabled_ = false;
|
||||
if(!(address & 1)) nmi_is_enabled_ = is_zx81;
|
||||
if(is_zx81 && !nmi_is_enabled_) z80_.set_wait_line(false);
|
||||
|
||||
// The below emulates the ZonX AY expansion device.
|
||||
if constexpr (is_zx81) {
|
||||
if((address&0xef) == 0xcf) {
|
||||
ay_set_register(*cycle.value);
|
||||
} else if((address&0xef) == 0x0f) {
|
||||
ay_set_data(*cycle.value);
|
||||
}
|
||||
}
|
||||
video_.run_for(horizontal_counter_ - vsync_end_);
|
||||
} else {
|
||||
video_.run_for(cycle.length);
|
||||
}
|
||||
break;
|
||||
|
||||
if constexpr (is_zx81) horizontal_counter_ %= HalfCycles(Cycles(207));
|
||||
if(!tape_advance_delay_) {
|
||||
tape_player_.run_for(cycle.length);
|
||||
} else {
|
||||
tape_advance_delay_ = std::max(tape_advance_delay_ - cycle.length, HalfCycles(0));
|
||||
}
|
||||
case CPU::Z80::PartialMachineCycle::Input: {
|
||||
uint8_t value = 0xff;
|
||||
if(!(address&1)) {
|
||||
if(!nmi_is_enabled_) set_vsync(true);
|
||||
|
||||
if(nmi_is_enabled_ && !z80_.get_halt_line() && z80_.get_non_maskable_interrupt_line()) {
|
||||
z80_.set_wait_line(true);
|
||||
}
|
||||
value &= keyboard_.read(address);
|
||||
value &= ~(tape_player_.input() ? 0x00 : 0x80);
|
||||
}
|
||||
|
||||
if(!cycle.is_terminal()) {
|
||||
return Cycles(0);
|
||||
}
|
||||
|
||||
const uint16_t address = cycle.address ? *cycle.address : 0;
|
||||
bool is_opcode_read = false;
|
||||
switch(cycle.operation) {
|
||||
case CPU::Z80::PartialMachineCycle::Output:
|
||||
if(!nmi_is_enabled_) {
|
||||
line_counter_ = 0;
|
||||
set_vsync(false);
|
||||
// The below emulates the ZonX AY expansion device.
|
||||
if constexpr (is_zx81) {
|
||||
if((address&0xef) == 0xcf) {
|
||||
value &= ay_read_data();
|
||||
}
|
||||
if(!(address & 2)) nmi_is_enabled_ = false;
|
||||
if(!(address & 1)) nmi_is_enabled_ = is_zx81;
|
||||
if(is_zx81 && !nmi_is_enabled_) z80_.set_wait_line(false);
|
||||
}
|
||||
*cycle.value = value;
|
||||
} break;
|
||||
|
||||
// The below emulates the ZonX AY expansion device.
|
||||
if constexpr (is_zx81) {
|
||||
if((address&0xef) == 0xcf) {
|
||||
ay_set_register(*cycle.value);
|
||||
} else if((address&0xef) == 0x0f) {
|
||||
ay_set_data(*cycle.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CPU::Z80::PartialMachineCycle::Interrupt:
|
||||
// resetting event is M1 and IOREQ both simultaneously having leading edges;
|
||||
// that happens 2 cycles before the end of INTACK. So the timer was reset and
|
||||
// now has advanced twice.
|
||||
horizontal_counter_ = HalfCycles(2);
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Input: {
|
||||
uint8_t value = 0xff;
|
||||
if(!(address&1)) {
|
||||
if(!nmi_is_enabled_) set_vsync(true);
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
|
||||
value &= keyboard_.read(address);
|
||||
value &= ~(tape_player_.input() ? 0x00 : 0x80);
|
||||
}
|
||||
|
||||
// The below emulates the ZonX AY expansion device.
|
||||
if constexpr (is_zx81) {
|
||||
if((address&0xef) == 0xcf) {
|
||||
value &= ay_read_data();
|
||||
}
|
||||
}
|
||||
*cycle.value = value;
|
||||
} break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Interrupt:
|
||||
// resetting event is M1 and IOREQ both simultaneously having leading edges;
|
||||
// that happens 2 cycles before the end of INTACK. So the timer was reset and
|
||||
// now has advanced twice.
|
||||
horizontal_counter_ = HalfCycles(2);
|
||||
|
||||
*cycle.value = 0xff;
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Refresh:
|
||||
// The ZX80 and 81 signal an interrupt while refresh is active and bit 6 of the refresh
|
||||
// address is low. The Z80 signals a refresh, providing the refresh address during the
|
||||
// final two cycles of an opcode fetch. Therefore communicate a transient signalling
|
||||
// of the IRQ line if necessary.
|
||||
if(!(address & 0x40)) {
|
||||
z80_.set_interrupt_line(true, Cycles(-2));
|
||||
z80_.set_interrupt_line(false);
|
||||
}
|
||||
if(has_latched_video_byte_) {
|
||||
std::size_t char_address = size_t((address & 0xfe00) | ((latched_video_byte_ & 0x3f) << 3) | line_counter_);
|
||||
const uint8_t mask = (latched_video_byte_ & 0x80) ? 0x00 : 0xff;
|
||||
if(char_address < ram_base_) {
|
||||
latched_video_byte_ = rom_[char_address & rom_mask_] ^ mask;
|
||||
} else {
|
||||
latched_video_byte_ = ram_[address & ram_mask_] ^ mask;
|
||||
}
|
||||
|
||||
video_.output_byte(latched_video_byte_);
|
||||
has_latched_video_byte_ = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
||||
// Check for use of the fast tape hack.
|
||||
if(use_fast_tape_hack_ && address == tape_trap_address_) {
|
||||
const uint64_t prior_offset = tape_player_.serialiser()->offset();
|
||||
const int next_byte = parser_.get_next_byte(*tape_player_.serialiser());
|
||||
if(next_byte != -1) {
|
||||
const uint16_t hl = z80_.value_of(CPU::Z80::Register::HL);
|
||||
ram_[hl & ram_mask_] = uint8_t(next_byte);
|
||||
*cycle.value = 0x00;
|
||||
z80_.set_value_of(CPU::Z80::Register::ProgramCounter, tape_return_address_ - 1);
|
||||
|
||||
// Assume that having read one byte quickly, we're probably going to be asked to read
|
||||
// another shortly. Therefore, temporarily disable the tape motor for 1000 cycles in order
|
||||
// to avoid fighting with real time. This is a stop-gap fix.
|
||||
tape_advance_delay_ = 1000;
|
||||
return 0;
|
||||
} else {
|
||||
tape_player_.serialiser()->set_offset(prior_offset);
|
||||
}
|
||||
}
|
||||
|
||||
if(should_autorun_ && address == finished_load_address_) {
|
||||
type_string(is_zx81 ? "r \n" : "r\n "); // Spaces here are not especially scientific; they merely ensure sufficient pauses for both the ZX80 and 81, empirically.
|
||||
should_autorun_ = false;
|
||||
}
|
||||
|
||||
// Check for automatic tape control.
|
||||
if(use_automatic_tape_motor_control_) {
|
||||
tape_player_.set_motor_control((address >= automatic_tape_motor_start_address_) && (address < automatic_tape_motor_end_address_));
|
||||
}
|
||||
is_opcode_read = true;
|
||||
[[fallthrough]];
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Read:
|
||||
if(address < ram_base_) {
|
||||
*cycle.value = rom_[address & rom_mask_];
|
||||
case CPU::Z80::PartialMachineCycle::Refresh:
|
||||
// The ZX80 and 81 signal an interrupt while refresh is active and bit 6 of the refresh
|
||||
// address is low. The Z80 signals a refresh, providing the refresh address during the
|
||||
// final two cycles of an opcode fetch. Therefore communicate a transient signalling
|
||||
// of the IRQ line if necessary.
|
||||
if(!(address & 0x40)) {
|
||||
z80_.set_interrupt_line(true, Cycles(-2));
|
||||
z80_.set_interrupt_line(false);
|
||||
}
|
||||
if(has_latched_video_byte_) {
|
||||
std::size_t char_address = size_t((address & 0xfe00) | ((latched_video_byte_ & 0x3f) << 3) | line_counter_);
|
||||
const uint8_t mask = (latched_video_byte_ & 0x80) ? 0x00 : 0xff;
|
||||
if(char_address < ram_base_) {
|
||||
latched_video_byte_ = rom_[char_address & rom_mask_] ^ mask;
|
||||
} else {
|
||||
const uint8_t value = ram_[address & ram_mask_];
|
||||
|
||||
// If this is an M1 cycle reading from above the 32kb mark and HALT is not
|
||||
// currently active, latch for video output and return a NOP. Otherwise,
|
||||
// just return the value as read.
|
||||
if(is_opcode_read && address&0x8000 && !(value & 0x40) && !z80_.get_halt_line()) {
|
||||
latched_video_byte_ = value;
|
||||
has_latched_video_byte_ = true;
|
||||
*cycle.value = 0;
|
||||
} else *cycle.value = value;
|
||||
latched_video_byte_ = ram_[address & ram_mask_] ^ mask;
|
||||
}
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Write:
|
||||
if(address >= ram_base_) {
|
||||
ram_[address & ram_mask_] = *cycle.value;
|
||||
}
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
if(typer_) typer_->run_for(cycle.length);
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Video) {
|
||||
video_.flush();
|
||||
}
|
||||
|
||||
if constexpr (is_zx81) {
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
video_.output_byte(latched_video_byte_);
|
||||
has_latched_video_byte_ = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
||||
// Check for use of the fast tape hack.
|
||||
if(use_fast_tape_hack_ && address == tape_trap_address_) {
|
||||
const uint64_t prior_offset = tape_player_.serialiser()->offset();
|
||||
const int next_byte = parser_.get_next_byte(*tape_player_.serialiser());
|
||||
if(next_byte != -1) {
|
||||
const uint16_t hl = z80_.value_of(CPU::Z80::Register::HL);
|
||||
ram_[hl & ram_mask_] = uint8_t(next_byte);
|
||||
*cycle.value = 0x00;
|
||||
z80_.set_value_of(CPU::Z80::Register::ProgramCounter, tape_return_address_ - 1);
|
||||
|
||||
// Assume that having read one byte quickly, we're probably going to be asked to read
|
||||
// another shortly. Therefore, temporarily disable the tape motor for 1000 cycles in order
|
||||
// to avoid fighting with real time. This is a stop-gap fix.
|
||||
tape_advance_delay_ = 1000;
|
||||
return 0;
|
||||
} else {
|
||||
tape_player_.serialiser()->set_offset(prior_offset);
|
||||
}
|
||||
}
|
||||
|
||||
if(should_autorun_ && address == finished_load_address_) {
|
||||
type_string(is_zx81 ? "r \n" : "r\n "); // Spaces here are not especially scientific; they merely ensure sufficient pauses for both the ZX80 and 81, empirically.
|
||||
should_autorun_ = false;
|
||||
}
|
||||
|
||||
// Check for automatic tape control.
|
||||
if(use_automatic_tape_motor_control_) {
|
||||
tape_player_.set_motor_control((address >= automatic_tape_motor_start_address_) && (address < automatic_tape_motor_end_address_));
|
||||
}
|
||||
is_opcode_read = true;
|
||||
[[fallthrough]];
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Read:
|
||||
if(address < ram_base_) {
|
||||
*cycle.value = rom_[address & rom_mask_];
|
||||
} else {
|
||||
const uint8_t value = ram_[address & ram_mask_];
|
||||
|
||||
// If this is an M1 cycle reading from above the 32kb mark and HALT is not
|
||||
// currently active, latch for video output and return a NOP. Otherwise,
|
||||
// just return the value as read.
|
||||
if(is_opcode_read && address&0x8000 && !(value & 0x40) && !z80_.get_halt_line()) {
|
||||
latched_video_byte_ = value;
|
||||
has_latched_video_byte_ = true;
|
||||
*cycle.value = 0;
|
||||
} else *cycle.value = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Write:
|
||||
if(address >= ram_base_) {
|
||||
ram_[address & ram_mask_] = *cycle.value;
|
||||
}
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
if(typer_) typer_->run_for(cycle.length);
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Video) {
|
||||
video_.flush();
|
||||
}
|
||||
|
||||
if constexpr (is_zx81) {
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_.set_scan_target(scan_target);
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return is_zx81 ? &speaker_ : nullptr;
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) final {
|
||||
z80_.run_for(cycles);
|
||||
}
|
||||
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
if(!media.tapes.empty()) {
|
||||
tape_player_.set_tape(media.tapes.front(), is_zx81 ? TargetPlatform::ZX81 : TargetPlatform::ZX80);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_.get_scaled_scan_status();
|
||||
set_use_fast_tape();
|
||||
return !media.tapes.empty();
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient<CharacterMapper>::add_typer(string);
|
||||
}
|
||||
|
||||
bool can_type(char c) const final {
|
||||
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
|
||||
}
|
||||
|
||||
// MARK: - Keyboard
|
||||
void set_key_state(uint16_t key, bool is_pressed) final {
|
||||
keyboard_.set_key_state(key, is_pressed);
|
||||
}
|
||||
|
||||
void clear_all_keys() final {
|
||||
keyboard_.clear_all_keys();
|
||||
}
|
||||
|
||||
static constexpr Sinclair::ZX::Keyboard::Machine keyboard_machine() {
|
||||
return is_zx81 ? Sinclair::ZX::Keyboard::Machine::ZX81 : Sinclair::ZX::Keyboard::Machine::ZX80;
|
||||
}
|
||||
|
||||
// MARK: - Tape control
|
||||
|
||||
void set_use_automatic_tape_motor_control(bool enabled) {
|
||||
use_automatic_tape_motor_control_ = enabled;
|
||||
if(!enabled) {
|
||||
tape_player_.set_motor_control(false);
|
||||
}
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return is_zx81 ? &speaker_ : nullptr;
|
||||
}
|
||||
void set_tape_is_playing(bool is_playing) final {
|
||||
tape_player_.set_motor_control(is_playing);
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) final {
|
||||
z80_.run_for(cycles);
|
||||
}
|
||||
bool get_tape_is_playing() final {
|
||||
return tape_player_.motor_control();
|
||||
}
|
||||
|
||||
bool insert_media(const Analyser::Static::Media &media) final {
|
||||
if(!media.tapes.empty()) {
|
||||
tape_player_.set_tape(media.tapes.front(), is_zx81 ? TargetPlatform::ZX81 : TargetPlatform::ZX80);
|
||||
}
|
||||
// MARK: - Typer timing
|
||||
HalfCycles get_typer_delay(const std::string &) const final {
|
||||
return z80_.get_is_resetting() ? Cycles(7'000'000) : Cycles(0);
|
||||
}
|
||||
|
||||
set_use_fast_tape();
|
||||
return !media.tapes.empty();
|
||||
}
|
||||
HalfCycles get_typer_frequency() const final {
|
||||
return Cycles(159'961);
|
||||
}
|
||||
|
||||
void type_string(const std::string &string) final {
|
||||
Utility::TypeRecipient<CharacterMapper>::add_typer(string);
|
||||
}
|
||||
KeyboardMapper *get_keyboard_mapper() final {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
|
||||
bool can_type(char c) const final {
|
||||
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
|
||||
}
|
||||
// MARK: - Configuration options.
|
||||
|
||||
// MARK: - Keyboard
|
||||
void set_key_state(uint16_t key, bool is_pressed) final {
|
||||
keyboard_.set_key_state(key, is_pressed);
|
||||
}
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly); // OptionsType is arbitrary, but not optional.
|
||||
options->automatic_tape_motor_control = use_automatic_tape_motor_control_;
|
||||
options->quickload = allow_fast_tape_hack_;
|
||||
return options;
|
||||
}
|
||||
|
||||
void clear_all_keys() final {
|
||||
keyboard_.clear_all_keys();
|
||||
}
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
set_use_automatic_tape_motor_control(options->automatic_tape_motor_control);
|
||||
allow_fast_tape_hack_ = options->quickload;
|
||||
set_use_fast_tape();
|
||||
}
|
||||
|
||||
static constexpr Sinclair::ZX::Keyboard::Machine keyboard_machine() {
|
||||
return is_zx81 ? Sinclair::ZX::Keyboard::Machine::ZX81 : Sinclair::ZX::Keyboard::Machine::ZX80;
|
||||
}
|
||||
private:
|
||||
CPU::Z80::Processor<ConcreteMachine, false, is_zx81> z80_;
|
||||
Video video_;
|
||||
|
||||
// MARK: - Tape control
|
||||
// If fast tape loading is enabled then the PC will be trapped at tape_trap_address_;
|
||||
// the emulator will then do a high-level reinterpretation of the standard ZX80/81 reading
|
||||
// of a single byte, and the next thing executed will be at tape_return_address_;
|
||||
static constexpr uint16_t tape_trap_address_ = is_zx81 ? 0x37c : 0x220;
|
||||
static constexpr uint16_t tape_return_address_ = is_zx81 ? 0x380 : 0x248;
|
||||
|
||||
void set_use_automatic_tape_motor_control(bool enabled) {
|
||||
use_automatic_tape_motor_control_ = enabled;
|
||||
if(!enabled) {
|
||||
tape_player_.set_motor_control(false);
|
||||
}
|
||||
}
|
||||
// If automatic tape motor control is enabled then the tape will be permitted to play any time
|
||||
// the program counter is >= automatic_tape_motor_start_address_ and < automatic_tape_motor_end_address_.
|
||||
static constexpr uint16_t automatic_tape_motor_start_address_ = is_zx81 ? 0x340 : 0x206;
|
||||
static constexpr uint16_t automatic_tape_motor_end_address_ = is_zx81 ? 0x3c3 : 0x24d;
|
||||
|
||||
void set_tape_is_playing(bool is_playing) final {
|
||||
tape_player_.set_motor_control(is_playing);
|
||||
}
|
||||
// When automatically loading, if the PC gets to the finished_load_address_ in order to print 0/0
|
||||
// (so it's anything that indicates that loading completed, but the program did not autorun) then the
|
||||
// emulator will automatically RUN whatever has been loaded.
|
||||
static constexpr uint16_t finished_load_address_ = is_zx81 ?
|
||||
0x6d1 : // ZX81: this is the routine that prints 0/0 (i.e. success).
|
||||
0x203; // ZX80: this is the JR that exits the ZX80's LOAD and returns to MAIN-EXEC.
|
||||
bool should_autorun_ = false;
|
||||
|
||||
bool get_tape_is_playing() final {
|
||||
return tape_player_.motor_control();
|
||||
}
|
||||
std::vector<uint8_t> ram_;
|
||||
uint16_t ram_mask_, ram_base_;
|
||||
|
||||
// MARK: - Typer timing
|
||||
HalfCycles get_typer_delay(const std::string &) const final {
|
||||
return z80_.get_is_resetting() ? Cycles(7'000'000) : Cycles(0);
|
||||
}
|
||||
std::vector<uint8_t> rom_;
|
||||
uint16_t rom_mask_;
|
||||
|
||||
HalfCycles get_typer_frequency() const final {
|
||||
return Cycles(159'961);
|
||||
}
|
||||
bool vsync_ = false, hsync_ = false;
|
||||
int line_counter_ = 0;
|
||||
|
||||
KeyboardMapper *get_keyboard_mapper() final {
|
||||
return &keyboard_mapper_;
|
||||
}
|
||||
Sinclair::ZX::Keyboard::Keyboard keyboard_;
|
||||
Sinclair::ZX::Keyboard::KeyboardMapper keyboard_mapper_;
|
||||
|
||||
// MARK: - Configuration options.
|
||||
HalfClockReceiver<Storage::Tape::BinaryTapePlayer> tape_player_;
|
||||
Storage::Tape::ZX8081::Parser parser_;
|
||||
|
||||
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly); // OptionsType is arbitrary, but not optional.
|
||||
options->automatic_tape_motor_control = use_automatic_tape_motor_control_;
|
||||
options->quickload = allow_fast_tape_hack_;
|
||||
return options;
|
||||
}
|
||||
bool nmi_is_enabled_ = false;
|
||||
|
||||
void set_options(const std::unique_ptr<Reflection::Struct> &str) {
|
||||
const auto options = dynamic_cast<Options *>(str.get());
|
||||
set_use_automatic_tape_motor_control(options->automatic_tape_motor_control);
|
||||
allow_fast_tape_hack_ = options->quickload;
|
||||
set_use_fast_tape();
|
||||
}
|
||||
static constexpr auto vsync_start_ = is_zx81 ? HalfCycles(32) : HalfCycles(26);
|
||||
static constexpr auto vsync_end_ = is_zx81 ? HalfCycles(64) : HalfCycles(66);
|
||||
HalfCycles horizontal_counter_;
|
||||
|
||||
private:
|
||||
CPU::Z80::Processor<ConcreteMachine, false, is_zx81> z80_;
|
||||
Video video_;
|
||||
uint8_t latched_video_byte_ = 0;
|
||||
bool has_latched_video_byte_ = false;
|
||||
|
||||
// If fast tape loading is enabled then the PC will be trapped at tape_trap_address_;
|
||||
// the emulator will then do a high-level reinterpretation of the standard ZX80/81 reading
|
||||
// of a single byte, and the next thing executed will be at tape_return_address_;
|
||||
static constexpr uint16_t tape_trap_address_ = is_zx81 ? 0x37c : 0x220;
|
||||
static constexpr uint16_t tape_return_address_ = is_zx81 ? 0x380 : 0x248;
|
||||
bool use_fast_tape_hack_ = false;
|
||||
bool allow_fast_tape_hack_ = false;
|
||||
void set_use_fast_tape() {
|
||||
use_fast_tape_hack_ = allow_fast_tape_hack_ && tape_player_.has_tape();
|
||||
}
|
||||
bool use_automatic_tape_motor_control_ = true;
|
||||
HalfCycles tape_advance_delay_ = 0;
|
||||
|
||||
// If automatic tape motor control is enabled then the tape will be permitted to play any time
|
||||
// the program counter is >= automatic_tape_motor_start_address_ and < automatic_tape_motor_end_address_.
|
||||
static constexpr uint16_t automatic_tape_motor_start_address_ = is_zx81 ? 0x340 : 0x206;
|
||||
static constexpr uint16_t automatic_tape_motor_end_address_ = is_zx81 ? 0x3c3 : 0x24d;
|
||||
// MARK: - Video
|
||||
inline void set_vsync(bool sync) {
|
||||
vsync_ = sync;
|
||||
update_sync();
|
||||
}
|
||||
|
||||
// When automatically loading, if the PC gets to the finished_load_address_ in order to print 0/0
|
||||
// (so it's anything that indicates that loading completed, but the program did not autorun) then the
|
||||
// emulator will automatically RUN whatever has been loaded.
|
||||
static constexpr uint16_t finished_load_address_ = is_zx81 ?
|
||||
0x6d1 : // ZX81: this is the routine that prints 0/0 (i.e. success).
|
||||
0x203; // ZX80: this is the JR that exits the ZX80's LOAD and returns to MAIN-EXEC.
|
||||
bool should_autorun_ = false;
|
||||
inline void set_hsync(bool sync) {
|
||||
hsync_ = sync;
|
||||
update_sync();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ram_;
|
||||
uint16_t ram_mask_, ram_base_;
|
||||
inline void update_sync() {
|
||||
video_.set_sync(vsync_ || hsync_);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> rom_;
|
||||
uint16_t rom_mask_;
|
||||
|
||||
bool vsync_ = false, hsync_ = false;
|
||||
int line_counter_ = 0;
|
||||
|
||||
Sinclair::ZX::Keyboard::Keyboard keyboard_;
|
||||
Sinclair::ZX::Keyboard::KeyboardMapper keyboard_mapper_;
|
||||
|
||||
HalfClockReceiver<Storage::Tape::BinaryTapePlayer> tape_player_;
|
||||
Storage::Tape::ZX8081::Parser parser_;
|
||||
|
||||
bool nmi_is_enabled_ = false;
|
||||
|
||||
static constexpr auto vsync_start_ = is_zx81 ? HalfCycles(32) : HalfCycles(26);
|
||||
static constexpr auto vsync_end_ = is_zx81 ? HalfCycles(64) : HalfCycles(66);
|
||||
HalfCycles horizontal_counter_;
|
||||
|
||||
uint8_t latched_video_byte_ = 0;
|
||||
bool has_latched_video_byte_ = false;
|
||||
|
||||
bool use_fast_tape_hack_ = false;
|
||||
bool allow_fast_tape_hack_ = false;
|
||||
void set_use_fast_tape() {
|
||||
use_fast_tape_hack_ = allow_fast_tape_hack_ && tape_player_.has_tape();
|
||||
}
|
||||
bool use_automatic_tape_motor_control_ = true;
|
||||
HalfCycles tape_advance_delay_ = 0;
|
||||
|
||||
// MARK: - Video
|
||||
inline void set_vsync(bool sync) {
|
||||
vsync_ = sync;
|
||||
update_sync();
|
||||
}
|
||||
|
||||
inline void set_hsync(bool sync) {
|
||||
hsync_ = sync;
|
||||
update_sync();
|
||||
}
|
||||
|
||||
inline void update_sync() {
|
||||
video_.set_sync(vsync_ || hsync_);
|
||||
}
|
||||
|
||||
// MARK: - Audio
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
using AY = GI::AY38910::AY38910<false>;
|
||||
AY ay_;
|
||||
Outputs::Speaker::PullLowpass<AY> speaker_;
|
||||
HalfCycles time_since_ay_update_;
|
||||
inline void ay_set_register(uint8_t value) {
|
||||
update_audio();
|
||||
GI::AY38910::Utility::select_register(ay_, value);
|
||||
}
|
||||
inline void ay_set_data(uint8_t value) {
|
||||
update_audio();
|
||||
GI::AY38910::Utility::write_data(ay_, value);
|
||||
}
|
||||
inline uint8_t ay_read_data() {
|
||||
update_audio();
|
||||
return GI::AY38910::Utility::read(ay_);
|
||||
}
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, time_since_ay_update_.divide_cycles(Cycles(2)));
|
||||
}
|
||||
// MARK: - Audio
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
using AY = GI::AY38910::AY38910<false>;
|
||||
AY ay_;
|
||||
Outputs::Speaker::PullLowpass<AY> speaker_;
|
||||
HalfCycles time_since_ay_update_;
|
||||
inline void ay_set_register(uint8_t value) {
|
||||
update_audio();
|
||||
GI::AY38910::Utility::select_register(ay_, value);
|
||||
}
|
||||
inline void ay_set_data(uint8_t value) {
|
||||
update_audio();
|
||||
GI::AY38910::Utility::write_data(ay_, value);
|
||||
}
|
||||
inline uint8_t ay_read_data() {
|
||||
update_audio();
|
||||
return GI::AY38910::Utility::read(ay_);
|
||||
}
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, time_since_ay_update_.divide_cycles(Cycles(2)));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -178,25 +178,25 @@ public:
|
||||
constexpr uint8_t masks[] = {0, 0xff};
|
||||
|
||||
#define Output(n) \
|
||||
{ \
|
||||
const uint8_t pixels = \
|
||||
uint8_t(last_fetches_[n] ^ masks[flash_mask_ & (last_fetches_[n+1] >> 7)]); \
|
||||
\
|
||||
const uint8_t colours[2] = { \
|
||||
palette[(last_fetches_[n+1] & 0x78) >> 3], \
|
||||
palette[((last_fetches_[n+1] & 0x40) >> 3) | (last_fetches_[n+1] & 0x07)], \
|
||||
}; \
|
||||
\
|
||||
pixel_target_[0] = colours[(pixels >> 7) & 1]; \
|
||||
pixel_target_[1] = colours[(pixels >> 6) & 1]; \
|
||||
pixel_target_[2] = colours[(pixels >> 5) & 1]; \
|
||||
pixel_target_[3] = colours[(pixels >> 4) & 1]; \
|
||||
pixel_target_[4] = colours[(pixels >> 3) & 1]; \
|
||||
pixel_target_[5] = colours[(pixels >> 2) & 1]; \
|
||||
pixel_target_[6] = colours[(pixels >> 1) & 1]; \
|
||||
pixel_target_[7] = colours[(pixels >> 0) & 1]; \
|
||||
pixel_target_ += 8; \
|
||||
}
|
||||
{ \
|
||||
const uint8_t pixels = \
|
||||
uint8_t(last_fetches_[n] ^ masks[flash_mask_ & (last_fetches_[n+1] >> 7)]); \
|
||||
\
|
||||
const uint8_t colours[2] = { \
|
||||
palette[(last_fetches_[n+1] & 0x78) >> 3], \
|
||||
palette[((last_fetches_[n+1] & 0x40) >> 3) | (last_fetches_[n+1] & 0x07)], \
|
||||
}; \
|
||||
\
|
||||
pixel_target_[0] = colours[(pixels >> 7) & 1]; \
|
||||
pixel_target_[1] = colours[(pixels >> 6) & 1]; \
|
||||
pixel_target_[2] = colours[(pixels >> 5) & 1]; \
|
||||
pixel_target_[3] = colours[(pixels >> 4) & 1]; \
|
||||
pixel_target_[4] = colours[(pixels >> 3) & 1]; \
|
||||
pixel_target_[5] = colours[(pixels >> 2) & 1]; \
|
||||
pixel_target_[6] = colours[(pixels >> 1) & 1]; \
|
||||
pixel_target_[7] = colours[(pixels >> 0) & 1]; \
|
||||
pixel_target_ += 8; \
|
||||
}
|
||||
|
||||
Output(0);
|
||||
Output(2);
|
||||
@@ -454,15 +454,14 @@ private:
|
||||
|
||||
friend struct State;
|
||||
|
||||
static constexpr uint8_t RGB(const uint8_t r, const uint8_t g, const uint8_t b) {
|
||||
return uint8_t((r << 4) | (g << 2) | b);
|
||||
}
|
||||
#define RGB(r, g, b) (r << 4) | (g << 2) | b
|
||||
static constexpr uint8_t palette[] = {
|
||||
RGB(0, 0, 0), RGB(0, 0, 2), RGB(2, 0, 0), RGB(2, 0, 2),
|
||||
RGB(0, 2, 0), RGB(0, 2, 2), RGB(2, 2, 0), RGB(2, 2, 2),
|
||||
RGB(0, 0, 0), RGB(0, 0, 3), RGB(3, 0, 0), RGB(3, 0, 3),
|
||||
RGB(0, 3, 0), RGB(0, 3, 3), RGB(3, 3, 0), RGB(3, 3, 3),
|
||||
};
|
||||
#undef RGB
|
||||
};
|
||||
|
||||
struct State: public Reflection::StructImpl<State> {
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ enum Name {
|
||||
AcornADFS,
|
||||
PRESAdvancedPlus6,
|
||||
Acorn1770DFS,
|
||||
AcornIDEADFS103,
|
||||
|
||||
// Acorn Archimedes.
|
||||
AcornArthur030,
|
||||
@@ -223,8 +224,20 @@ struct Description {
|
||||
|
||||
private:
|
||||
template <typename FileNameT, typename CRC32T> Description(
|
||||
Name name, std::string machine_name, std::string descriptive_name, FileNameT file_names, size_t size, CRC32T crc32s = uint32_t(0)
|
||||
) : name{name}, machine_name{machine_name}, descriptive_name{descriptive_name}, file_names{file_names}, size{size}, crc32s{crc32s} {
|
||||
const Name name,
|
||||
const char *machine_name,
|
||||
const char *descriptive_name,
|
||||
const FileNameT &file_names,
|
||||
const size_t size,
|
||||
const CRC32T crc32s = uint32_t(0)
|
||||
) :
|
||||
name{name},
|
||||
machine_name{machine_name},
|
||||
descriptive_name{descriptive_name},
|
||||
file_names{file_names},
|
||||
size{size},
|
||||
crc32s{crc32s}
|
||||
{
|
||||
// Slightly lazy: deal with the case where the constructor wasn't provided with any
|
||||
// CRCs by spotting that the set has exactly one member, which has value 0. The alternative
|
||||
// would be to provide a partial specialisation that never put anything into the set.
|
||||
@@ -232,6 +245,8 @@ private:
|
||||
this->crc32s.clear();
|
||||
}
|
||||
}
|
||||
|
||||
static const std::vector<Description> &all_roms();
|
||||
};
|
||||
|
||||
/// @returns a vector of all possible instances of ROM::Description — i.e. descriptions of every ROM
|
||||
|
@@ -22,37 +22,37 @@ namespace Utility {
|
||||
necessary to type that character on a given machine.
|
||||
*/
|
||||
class CharacterMapper {
|
||||
public:
|
||||
virtual ~CharacterMapper() = default;
|
||||
public:
|
||||
virtual ~CharacterMapper() = default;
|
||||
|
||||
/// @returns The EndSequence-terminated sequence of keys that would cause @c character to be typed.
|
||||
virtual const uint16_t *sequence_for_character(char character) const = 0;
|
||||
/// @returns The EndSequence-terminated sequence of keys that would cause @c character to be typed.
|
||||
virtual const uint16_t *sequence_for_character(char character) const = 0;
|
||||
|
||||
/// The typer will automatically reset all keys in between each sequence that it types.
|
||||
/// By default it will pause for one key's duration when doing so. Character mappers
|
||||
/// can eliminate that pause by overriding this method.
|
||||
/// @returns @c true if the typer should pause after performing a reset; @c false otherwise.
|
||||
virtual bool needs_pause_after_reset_all_keys() const { return true; }
|
||||
/// The typer will automatically reset all keys in between each sequence that it types.
|
||||
/// By default it will pause for one key's duration when doing so. Character mappers
|
||||
/// can eliminate that pause by overriding this method.
|
||||
/// @returns @c true if the typer should pause after performing a reset; @c false otherwise.
|
||||
virtual bool needs_pause_after_reset_all_keys() const { return true; }
|
||||
|
||||
/// The typer will pause between every entry in a keyboard sequence. On some machines
|
||||
/// that may not be necessary — it'll often depends on whether the machine needs time to
|
||||
/// observe a modifier like shift before it sees the actual keypress.
|
||||
/// @returns @c true if the typer should pause after forwarding @c key; @c false otherwise.
|
||||
virtual bool needs_pause_after_key([[maybe_unused]] uint16_t key) const { return true; }
|
||||
/// The typer will pause between every entry in a keyboard sequence. On some machines
|
||||
/// that may not be necessary — it'll often depends on whether the machine needs time to
|
||||
/// observe a modifier like shift before it sees the actual keypress.
|
||||
/// @returns @c true if the typer should pause after forwarding @c key; @c false otherwise.
|
||||
virtual bool needs_pause_after_key([[maybe_unused]] uint16_t key) const { return true; }
|
||||
|
||||
protected:
|
||||
using KeySequence = std::array<uint16_t, 16>;
|
||||
protected:
|
||||
using KeySequence = std::array<uint16_t, 16>;
|
||||
|
||||
/*!
|
||||
Provided in the base class as a convenience: given the C array of key sequences @c sequences,
|
||||
returns the sequence for character @c character if it exists; otherwise returns @c nullptr.
|
||||
*/
|
||||
template <typename Collection> const uint16_t *table_lookup_sequence_for_character(const Collection &sequences, char character) const {
|
||||
std::size_t ucharacter = size_t((unsigned char)character);
|
||||
if(ucharacter >= sizeof(sequences) / sizeof(KeySequence)) return nullptr;
|
||||
if(sequences[ucharacter][0] == MachineTypes::MappedKeyboardMachine::KeyNotMapped) return nullptr;
|
||||
return sequences[ucharacter].data();
|
||||
}
|
||||
/*!
|
||||
Provided in the base class as a convenience: given the C array of key sequences @c sequences,
|
||||
returns the sequence for character @c character if it exists; otherwise returns @c nullptr.
|
||||
*/
|
||||
template <typename Collection> const uint16_t *table_lookup_sequence_for_character(const Collection &sequences, char character) const {
|
||||
std::size_t ucharacter = size_t((unsigned char)character);
|
||||
if(ucharacter >= sizeof(sequences) / sizeof(KeySequence)) return nullptr;
|
||||
if(sequences[ucharacter][0] == MachineTypes::MappedKeyboardMachine::KeyNotMapped) return nullptr;
|
||||
return sequences[ucharacter].data();
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@@ -16,7 +16,7 @@ namespace Numeric {
|
||||
/// keeping the least-significant bit in its original position.
|
||||
///
|
||||
/// i.e. if @c input is abcdefgh then the result is 0a0b0c0d0e0f0g0h
|
||||
constexpr uint16_t spread_bits(uint8_t input) {
|
||||
constexpr uint16_t spread_bits(const uint8_t input) {
|
||||
uint16_t result = uint16_t(input); // 0000 0000 abcd efgh
|
||||
result = (result | (result << 4)) & 0x0f0f; // 0000 abcd 0000 efgh
|
||||
result = (result | (result << 2)) & 0x3333; // 00ab 00cd 00ef 00gh
|
||||
@@ -26,11 +26,12 @@ constexpr uint16_t spread_bits(uint8_t input) {
|
||||
/// Performs the opposite action to @c spread_bits; given the 16-bit input
|
||||
/// @c abcd @c efgh @c ijkl @c mnop, returns the byte value @c bdfhjlnp
|
||||
/// i.e. every other bit is retained, keeping the least-significant bit in place.
|
||||
constexpr uint8_t unspread_bits(uint16_t input) {
|
||||
input &= 0x5555; // 0a0b 0c0d 0e0f 0g0h
|
||||
input = (input | (input >> 1)) & 0x3333; // 00ab 00cd 00ef 00gh
|
||||
input = (input | (input >> 2)) & 0x0f0f; // 0000 abcd 0000 efgh
|
||||
return uint8_t(input | (input >> 4)); // 0000 0000 abcd efgh
|
||||
constexpr uint8_t unspread_bits(const uint16_t input) {
|
||||
uint16_t result = input;
|
||||
result &= 0x5555; // 0a0b 0c0d 0e0f 0g0h
|
||||
result = (result | (result >> 1)) & 0x3333; // 00ab 00cd 00ef 00gh
|
||||
result = (result | (result >> 2)) & 0x0f0f; // 0000 abcd 0000 efgh
|
||||
return uint8_t(result | (result >> 4)); // 0000 0000 abcd efgh
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -61,7 +61,7 @@ public:
|
||||
|
||||
An initial value of 0 is invalid.
|
||||
*/
|
||||
LFSR(IntType initial_value) : value_(initial_value) {}
|
||||
LFSR(const IntType initial_value) : value_(initial_value) {}
|
||||
|
||||
/*!
|
||||
Advances the LSFR, returning either an @c IntType of value @c 1 or @c 0,
|
||||
|
@@ -30,13 +30,13 @@ namespace Numeric {
|
||||
template <int... Sizes> class NumericCoder {
|
||||
public:
|
||||
/// Modifies @c target to hold @c value at @c index.
|
||||
template <int index> static void encode(int &target, int value) {
|
||||
template <int index> static void encode(int &target, const int value) {
|
||||
static_assert(index < sizeof...(Sizes), "Index must be within range");
|
||||
NumericEncoder<Sizes...>::template encode<index>(target, value);
|
||||
}
|
||||
|
||||
/// @returns The value from @c source at @c index.
|
||||
template <int index> static int decode(int source) {
|
||||
template <int index> static int decode(const int source) {
|
||||
static_assert(index < sizeof...(Sizes), "Index must be within range");
|
||||
return NumericDecoder<Sizes...>::template decode<index>(source);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ private:
|
||||
|
||||
template <int size, int... Tail>
|
||||
struct NumericEncoder {
|
||||
template <int index, int i = 0, int divider = 1> static void encode(int &target, int value) {
|
||||
template <int index, int i = 0, int divider = 1> static void encode(int &target, const int value) {
|
||||
if constexpr (i == index) {
|
||||
const int suffix = target % divider;
|
||||
target /= divider;
|
||||
@@ -61,7 +61,7 @@ private:
|
||||
|
||||
template <int size, int... Tail>
|
||||
struct NumericDecoder {
|
||||
template <int index, int i = 0, int divider = 1> static int decode(int source) {
|
||||
template <int index, int i = 0, int divider = 1> static int decode(const int source) {
|
||||
if constexpr (i == index) {
|
||||
return (source / divider) % size;
|
||||
} else {
|
||||
|
@@ -62,7 +62,7 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableASanStackUseAfterReturn = "YES"
|
||||
|
@@ -76,7 +76,7 @@ enum class EnabledLevel {
|
||||
|
||||
constexpr EnabledLevel enabled_level(const Source source) {
|
||||
#ifdef NDEBUG
|
||||
return false;
|
||||
return EnabledLevel::None;
|
||||
#endif
|
||||
|
||||
// Allow for compile-time source-level enabling and disabling of different sources.
|
||||
|
@@ -24,7 +24,7 @@ GLuint Shader::compile_shader(const std::string &source, GLenum type) {
|
||||
test_gl(glShaderSource, shader, 1, &c_str, NULL);
|
||||
test_gl(glCompileShader, shader);
|
||||
|
||||
if constexpr (logger.enabled) {
|
||||
if constexpr (logger.ErrorsEnabled) {
|
||||
GLint isCompiled = 0;
|
||||
test_gl(glGetShaderiv, shader, GL_COMPILE_STATUS, &isCompiled);
|
||||
if(isCompiled == GL_FALSE) {
|
||||
@@ -69,7 +69,7 @@ void Shader::init(const std::string &vertex_shader, const std::string &fragment_
|
||||
for(const auto &binding : attribute_bindings) {
|
||||
test_gl(glBindAttribLocation, shader_program_, binding.index, binding.name.c_str());
|
||||
|
||||
if constexpr (logger.enabled) {
|
||||
if constexpr (logger.ErrorsEnabled) {
|
||||
const auto error = glGetError();
|
||||
switch(error) {
|
||||
case 0: break;
|
||||
@@ -88,7 +88,7 @@ void Shader::init(const std::string &vertex_shader, const std::string &fragment_
|
||||
|
||||
test_gl(glLinkProgram, shader_program_);
|
||||
|
||||
if constexpr (logger.enabled) {
|
||||
if constexpr (logger.ErrorsEnabled) {
|
||||
GLint logLength;
|
||||
test_gl(glGetProgramiv, shader_program_, GL_INFO_LOG_LENGTH, &logLength);
|
||||
if(logLength > 0) {
|
||||
|
@@ -56,45 +56,45 @@ template <Action action, typename IteratorT, typename SampleT> void fill(Iterato
|
||||
*/
|
||||
template <typename SourceT, bool stereo>
|
||||
class BufferSource {
|
||||
public:
|
||||
/*!
|
||||
Indicates whether this component will write stereo samples.
|
||||
*/
|
||||
static constexpr bool is_stereo = stereo;
|
||||
public:
|
||||
/*!
|
||||
Indicates whether this component will write stereo samples.
|
||||
*/
|
||||
static constexpr bool is_stereo = stereo;
|
||||
|
||||
/*!
|
||||
Should 'apply' the next @c number_of_samples to @c target ; application means applying @c action which can be achieved either via the
|
||||
helper functions above — @c apply and @c fill — or by semantic inspection (primarily, if an obvious quick route for @c Action::Ignore is available).
|
||||
/*!
|
||||
Should 'apply' the next @c number_of_samples to @c target ; application means applying @c action which can be achieved either via the
|
||||
helper functions above — @c apply and @c fill — or by semantic inspection (primarily, if an obvious quick route for @c Action::Ignore is available).
|
||||
|
||||
No default implementation is provided.
|
||||
*/
|
||||
template <Action action>
|
||||
void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *target);
|
||||
No default implementation is provided.
|
||||
*/
|
||||
template <Action action>
|
||||
void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *target);
|
||||
|
||||
/*!
|
||||
@returns @c true if it is trivially true that a call to get_samples would just
|
||||
fill the target with zeroes; @c false if a call might return all zeroes or
|
||||
might not.
|
||||
*/
|
||||
// bool is_zero_level() const { return false; }
|
||||
/*!
|
||||
@returns @c true if it is trivially true that a call to get_samples would just
|
||||
fill the target with zeroes; @c false if a call might return all zeroes or
|
||||
might not.
|
||||
*/
|
||||
// bool is_zero_level() const { return false; }
|
||||
|
||||
/*!
|
||||
Sets the proper output range for this sample source; it should write values
|
||||
between 0 and volume.
|
||||
*/
|
||||
// void set_sample_volume_range(std::int16_t volume);
|
||||
/*!
|
||||
Sets the proper output range for this sample source; it should write values
|
||||
between 0 and volume.
|
||||
*/
|
||||
// void set_sample_volume_range(std::int16_t volume);
|
||||
|
||||
/*!
|
||||
Permits a sample source to declare that, averaged over time, it will use only
|
||||
a certain proportion of the allocated volume range. This commonly happens
|
||||
in sample sources that use a time-multiplexed sound output — for example, if
|
||||
one were to output only every other sample then it would return 0.5.
|
||||
/*!
|
||||
Permits a sample source to declare that, averaged over time, it will use only
|
||||
a certain proportion of the allocated volume range. This commonly happens
|
||||
in sample sources that use a time-multiplexed sound output — for example, if
|
||||
one were to output only every other sample then it would return 0.5.
|
||||
|
||||
This is permitted to vary over time but there is no contract as to when it will be
|
||||
used by a speaker. If it varies, it should do so very infrequently and only to
|
||||
represent changes in hardware configuration.
|
||||
*/
|
||||
double average_output_peak() const { return 1.0; }
|
||||
This is permitted to vary over time but there is no contract as to when it will be
|
||||
used by a speaker. If it varies, it should do so very infrequently and only to
|
||||
represent changes in hardware configuration.
|
||||
*/
|
||||
double average_output_peak() const { return 1.0; }
|
||||
};
|
||||
|
||||
///
|
||||
|
@@ -8,7 +8,8 @@ DFS-1770-2.20.rom — used only if the user opens a DFS disk image
|
||||
ADFS-E00_1.rom — used only if the user opens an ADFS disk image
|
||||
ADFS-E00_2.rom
|
||||
AP6v133.rom — used only if the user opens a disk image that makes use of any of the commands given below.
|
||||
adfs.rom - used only if the user opens a hard disk image
|
||||
adfs.rom — used only if the user opens a hard disk image or requests a SCSI interface
|
||||
ELK103.rom — used only if the user requests an IDE interface
|
||||
|
||||
Possibly to be desired in the future:
|
||||
* os300.rom
|
||||
|
Reference in New Issue
Block a user