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> {
|
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
|
||||||
friend Configurable::DisplayOption<Options>;
|
friend Configurable::DisplayOption<Options>;
|
||||||
public:
|
public:
|
||||||
Options(Configurable::OptionsType type) :
|
Options(Configurable::OptionsType type) :
|
||||||
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ?
|
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ?
|
||||||
Configurable::Display::SVideo : Configurable::Display::CompositeColour) {}
|
Configurable::Display::SVideo : Configurable::Display::CompositeColour) {}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
Options() : Options(Configurable::OptionsType::UserFriendly) {}
|
||||||
|
@@ -44,37 +44,37 @@ namespace {
|
|||||||
Log::Logger<Log::Source::Plus4> logger;
|
Log::Logger<Log::Source::Plus4> logger;
|
||||||
|
|
||||||
class Joystick: public Inputs::ConcreteJoystick {
|
class Joystick: public Inputs::ConcreteJoystick {
|
||||||
public:
|
public:
|
||||||
Joystick() :
|
Joystick() :
|
||||||
ConcreteJoystick({
|
ConcreteJoystick({
|
||||||
Input(Input::Up),
|
Input(Input::Up),
|
||||||
Input(Input::Down),
|
Input(Input::Down),
|
||||||
Input(Input::Left),
|
Input(Input::Left),
|
||||||
Input(Input::Right),
|
Input(Input::Right),
|
||||||
Input(Input::Fire)
|
Input(Input::Fire)
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||||
const auto apply = [&](uint8_t mask) {
|
const auto apply = [&](uint8_t mask) {
|
||||||
if(is_active) mask_ &= ~mask; else mask_ |= mask;
|
if(is_active) mask_ &= ~mask; else mask_ |= mask;
|
||||||
};
|
};
|
||||||
|
|
||||||
switch(digital_input.type) {
|
switch(digital_input.type) {
|
||||||
default: return;
|
default: return;
|
||||||
case Input::Right: apply(0x08); break;
|
case Input::Right: apply(0x08); break;
|
||||||
case Input::Left: apply(0x04); break;
|
case Input::Left: apply(0x04); break;
|
||||||
case Input::Down: apply(0x02); break;
|
case Input::Down: apply(0x02); break;
|
||||||
case Input::Up: apply(0x01); break;
|
case Input::Up: apply(0x01); break;
|
||||||
case Input::Fire: apply(0xc0); break;
|
case Input::Fire: apply(0xc0); break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t mask() const {
|
uint8_t mask() const {
|
||||||
return mask_;
|
return mask_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint8_t mask_ = 0xff;
|
uint8_t mask_ = 0xff;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Timers {
|
class Timers {
|
||||||
|
@@ -13,35 +13,35 @@
|
|||||||
namespace MSX::Cartridge {
|
namespace MSX::Cartridge {
|
||||||
|
|
||||||
class ASCII16kbROMSlotHandler: public MemorySlotHandler {
|
class ASCII16kbROMSlotHandler: public MemorySlotHandler {
|
||||||
public:
|
public:
|
||||||
ASCII16kbROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
ASCII16kbROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
||||||
|
|
||||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
void write(const uint16_t address, const uint8_t value, const bool pc_is_outside_bios) final {
|
||||||
switch(address >> 11) {
|
switch(address >> 11) {
|
||||||
default:
|
default:
|
||||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||||
break;
|
break;
|
||||||
case 0xc:
|
case 0xc:
|
||||||
if(pc_is_outside_bios) {
|
if(pc_is_outside_bios) {
|
||||||
if(address == 0x6000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
hit_or_equivocal(address == 0x6000);
|
||||||
}
|
}
|
||||||
slot_.map(value * 0x4000, 0x4000, 0x4000);
|
slot_.map(value * 0x4000, 0x4000, 0x4000);
|
||||||
break;
|
break;
|
||||||
case 0xe:
|
case 0xe:
|
||||||
if(pc_is_outside_bios) {
|
if(pc_is_outside_bios) {
|
||||||
if(address == 0x7000 || address == 0x77ff) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
hit_or_equivocal(address == 0x7000 || address == 0x77ff);
|
||||||
}
|
}
|
||||||
slot_.map(value * 0x4000, 0x8000, 0x4000);
|
slot_.map(value * 0x4000, 0x8000, 0x4000);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
virtual std::string debug_type() final {
|
virtual std::string debug_type() final {
|
||||||
return "A16";
|
return "A16";
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MSX::MemorySlot &slot_;
|
MSX::MemorySlot &slot_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -13,47 +13,47 @@
|
|||||||
namespace MSX::Cartridge {
|
namespace MSX::Cartridge {
|
||||||
|
|
||||||
class ASCII8kbROMSlotHandler: public MemorySlotHandler {
|
class ASCII8kbROMSlotHandler: public MemorySlotHandler {
|
||||||
public:
|
public:
|
||||||
ASCII8kbROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
ASCII8kbROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
||||||
|
|
||||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
void write(const uint16_t address, const uint8_t value, const bool pc_is_outside_bios) final {
|
||||||
switch(address >> 11) {
|
switch(address >> 11) {
|
||||||
default:
|
default:
|
||||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||||
break;
|
break;
|
||||||
case 0xc:
|
case 0xc:
|
||||||
if(pc_is_outside_bios) {
|
if(pc_is_outside_bios) {
|
||||||
if(address == 0x6000 || address == 0x60ff) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
hit_or_equivocal(address == 0x6000 || address == 0x60ff);
|
||||||
}
|
}
|
||||||
slot_.map(value * 0x2000, 0x4000, 0x2000);
|
slot_.map(value * 0x2000, 0x4000, 0x2000);
|
||||||
break;
|
break;
|
||||||
case 0xd:
|
case 0xd:
|
||||||
if(pc_is_outside_bios) {
|
if(pc_is_outside_bios) {
|
||||||
if(address == 0x6800 || address == 0x68ff) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
hit_or_equivocal(address == 0x6800 || address == 0x68ff);
|
||||||
}
|
}
|
||||||
slot_.map(value * 0x2000, 0x6000, 0x2000);
|
slot_.map(value * 0x2000, 0x6000, 0x2000);
|
||||||
break;
|
break;
|
||||||
case 0xe:
|
case 0xe:
|
||||||
if(pc_is_outside_bios) {
|
if(pc_is_outside_bios) {
|
||||||
if(address == 0x7000 || address == 0x70ff) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
hit_or_equivocal(address == 0x7000 || address == 0x70ff);
|
||||||
}
|
}
|
||||||
slot_.map(value * 0x2000, 0x8000, 0x2000);
|
slot_.map(value * 0x2000, 0x8000, 0x2000);
|
||||||
break;
|
break;
|
||||||
case 0xf:
|
case 0xf:
|
||||||
if(pc_is_outside_bios) {
|
if(pc_is_outside_bios) {
|
||||||
if(address == 0x7800 || address == 0x78ff) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
hit_or_equivocal(address == 0x7800 || address == 0x78ff);
|
||||||
}
|
}
|
||||||
slot_.map(value * 0x2000, 0xa000, 0x2000);
|
slot_.map(value * 0x2000, 0xa000, 0x2000);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
virtual std::string debug_type() final {
|
virtual std::string debug_type() final {
|
||||||
return "A8";
|
return "A8";
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MSX::MemorySlot &slot_;
|
MSX::MemorySlot &slot_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -13,39 +13,40 @@
|
|||||||
namespace MSX::Cartridge {
|
namespace MSX::Cartridge {
|
||||||
|
|
||||||
class KonamiROMSlotHandler: public MemorySlotHandler {
|
class KonamiROMSlotHandler: public MemorySlotHandler {
|
||||||
public:
|
public:
|
||||||
KonamiROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
KonamiROMSlotHandler(MSX::MemorySlot &slot) : slot_(slot) {}
|
||||||
|
|
||||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
||||||
switch(address >> 13) {
|
switch(address >> 13) {
|
||||||
default:
|
default:
|
||||||
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
if(pc_is_outside_bios) {
|
if(pc_is_outside_bios) {
|
||||||
if(address == 0x6000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
hit_or_equivocal(address == 0x6000);
|
||||||
}
|
}
|
||||||
slot_.map(value * 0x2000, 0x6000, 0x2000);
|
slot_.map(value * 0x2000, 0x6000, 0x2000);
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
if(pc_is_outside_bios) {
|
if(pc_is_outside_bios) {
|
||||||
if(address == 0x8000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
hit_or_equivocal(address == 0x8000);
|
||||||
}
|
}
|
||||||
slot_.map(value * 0x2000, 0x8000, 0x2000);
|
slot_.map(value * 0x2000, 0x8000, 0x2000);
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
if(pc_is_outside_bios) {
|
if(pc_is_outside_bios) {
|
||||||
if(address == 0xa000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
hit_or_equivocal(address == 0xa000);
|
||||||
}
|
}
|
||||||
slot_.map(value * 0x2000, 0xa000, 0x2000);
|
slot_.map(value * 0x2000, 0xa000, 0x2000);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
virtual std::string debug_type() final {
|
virtual std::string debug_type() final {
|
||||||
return "K";
|
return "K";
|
||||||
}
|
}
|
||||||
private:
|
|
||||||
|
private:
|
||||||
MSX::MemorySlot &slot_;
|
MSX::MemorySlot &slot_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -14,73 +14,73 @@
|
|||||||
namespace MSX::Cartridge {
|
namespace MSX::Cartridge {
|
||||||
|
|
||||||
class KonamiWithSCCROMSlotHandler: public MemorySlotHandler {
|
class KonamiWithSCCROMSlotHandler: public MemorySlotHandler {
|
||||||
public:
|
public:
|
||||||
KonamiWithSCCROMSlotHandler(MSX::MemorySlot &slot, Konami::SCC &scc) :
|
KonamiWithSCCROMSlotHandler(MSX::MemorySlot &slot, Konami::SCC &scc) :
|
||||||
slot_(slot), scc_(scc) {}
|
slot_(slot), scc_(scc) {}
|
||||||
|
|
||||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
|
void write(const uint16_t address, const uint8_t value, const bool pc_is_outside_bios) final {
|
||||||
switch(address >> 11) {
|
switch(address >> 11) {
|
||||||
default:
|
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();
|
if(pc_is_outside_bios) confidence_counter_.add_miss();
|
||||||
break;
|
}
|
||||||
case 0x0a:
|
break;
|
||||||
if(pc_is_outside_bios) {
|
case 0x16:
|
||||||
if(address == 0x5000) confidence_counter_.add_hit(); else confidence_counter_.add_equivocal();
|
if(pc_is_outside_bios) {
|
||||||
}
|
hit_or_equivocal(address == 0xb000);
|
||||||
slot_.map(value * 0x2000, 0x4000, 0x2000);
|
}
|
||||||
break;
|
slot_.map(value * 0x2000, 0xa000, 0x2000);
|
||||||
case 0x0e:
|
break;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t read(uint16_t address) final {
|
uint8_t read(uint16_t address) final {
|
||||||
if(scc_is_visible_ && address >= 0x9800 && address < 0xa000) {
|
if(scc_is_visible_ && address >= 0x9800 && address < 0xa000) {
|
||||||
confidence_counter_.add_hit();
|
confidence_counter_.add_hit();
|
||||||
return scc_.read(address);
|
return scc_.read(address);
|
||||||
}
|
|
||||||
confidence_counter_.add_miss();
|
|
||||||
return 0xff;
|
|
||||||
}
|
}
|
||||||
|
confidence_counter_.add_miss();
|
||||||
|
return 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
virtual std::string debug_type() final {
|
virtual std::string debug_type() final {
|
||||||
return "KSCC";
|
return "KSCC";
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MSX::MemorySlot &slot_;
|
MSX::MemorySlot &slot_;
|
||||||
Konami::SCC &scc_;
|
Konami::SCC &scc_;
|
||||||
bool scc_is_visible_ = false;
|
bool scc_is_visible_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -21,22 +21,22 @@
|
|||||||
namespace MSX {
|
namespace MSX {
|
||||||
|
|
||||||
class DiskROM: public MemorySlotHandler, public WD::WD1770 {
|
class DiskROM: public MemorySlotHandler, public WD::WD1770 {
|
||||||
public:
|
public:
|
||||||
DiskROM(MSX::MemorySlot &slot);
|
DiskROM(MSX::MemorySlot &slot);
|
||||||
|
|
||||||
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final;
|
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final;
|
||||||
uint8_t read(uint16_t address) final;
|
uint8_t read(uint16_t address) final;
|
||||||
void run_for(HalfCycles half_cycles) final;
|
void run_for(HalfCycles half_cycles) final;
|
||||||
|
|
||||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
|
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
|
||||||
void set_activity_observer(Activity::Observer *observer);
|
void set_activity_observer(Activity::Observer *observer);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::vector<uint8_t> &rom_;
|
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 {
|
class MemorySlot {
|
||||||
public:
|
public:
|
||||||
MemorySlot(MemorySlotChangeHandler &);
|
MemorySlot(MemorySlotChangeHandler &);
|
||||||
|
|
||||||
/// @returns A pointer to the area of memory currently underneath @c address that
|
/// @returns A pointer to the area of memory currently underneath @c address that
|
||||||
/// should be read
|
/// should be read
|
||||||
const uint8_t *read_pointer(int segment) const;
|
const uint8_t *read_pointer(int segment) const;
|
||||||
|
|
||||||
/// @returns A pointer to the area of memory currently underneath @c address.
|
/// @returns A pointer to the area of memory currently underneath @c address.
|
||||||
uint8_t *write_pointer(int segment) const;
|
uint8_t *write_pointer(int segment) const;
|
||||||
|
|
||||||
/// Copies an underlying source buffer.
|
/// Copies an underlying source buffer.
|
||||||
void set_source(const std::vector<uint8_t> &source);
|
void set_source(const std::vector<uint8_t> &source);
|
||||||
|
|
||||||
/// Sets the size of the underlying source buffer.
|
/// Sets the size of the underlying source buffer.
|
||||||
void resize_source(std::size_t);
|
void resize_source(std::size_t);
|
||||||
|
|
||||||
/// Provides a reference to the internal source storage.
|
/// Provides a reference to the internal source storage.
|
||||||
std::vector<uint8_t> &source();
|
std::vector<uint8_t> &source();
|
||||||
const std::vector<uint8_t> &source() const;
|
const std::vector<uint8_t> &source() const;
|
||||||
|
|
||||||
enum AccessType {
|
enum AccessType {
|
||||||
Read,
|
Read,
|
||||||
ReadWrite
|
ReadWrite
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Maps the content from @c source_address in the buffer previously
|
/// Maps the content from @c source_address in the buffer previously
|
||||||
/// supplied to @c set_source to the region indicated by
|
/// supplied to @c set_source to the region indicated by
|
||||||
/// @c destination_address and @c length within @c subslot.
|
/// @c destination_address and @c length within @c subslot.
|
||||||
template <AccessType type = AccessType::Read> void map(
|
template <AccessType type = AccessType::Read> void map(
|
||||||
std::size_t source_address,
|
std::size_t source_address,
|
||||||
uint16_t destination_address,
|
uint16_t destination_address,
|
||||||
std::size_t length);
|
std::size_t length);
|
||||||
|
|
||||||
/// Marks the region indicated by @c destination_address and @c length
|
/// Marks the region indicated by @c destination_address and @c length
|
||||||
/// as requiring calls into this slot's MemorySlotHandler.
|
/// as requiring calls into this slot's MemorySlotHandler.
|
||||||
void map_handler(
|
void map_handler(
|
||||||
uint16_t destination_address,
|
uint16_t destination_address,
|
||||||
std::size_t length);
|
std::size_t length);
|
||||||
|
|
||||||
/// Marks the region indicated by @c destination_address and @c length
|
/// Marks the region indicated by @c destination_address and @c length
|
||||||
/// as unoccupied.
|
/// as unoccupied.
|
||||||
void unmap(
|
void unmap(
|
||||||
uint16_t destination_address,
|
uint16_t destination_address,
|
||||||
std::size_t length);
|
std::size_t length);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<uint8_t> source_;
|
std::vector<uint8_t> source_;
|
||||||
uint8_t *read_pointers_[8];
|
uint8_t *read_pointers_[8];
|
||||||
uint8_t *write_pointers_[8];
|
uint8_t *write_pointers_[8];
|
||||||
|
|
||||||
MemorySlotChangeHandler &handler_;
|
MemorySlotChangeHandler &handler_;
|
||||||
|
|
||||||
using MemoryChunk = std::array<uint8_t, 8192>;
|
using MemoryChunk = std::array<uint8_t, 8192>;
|
||||||
inline static MemoryChunk unmapped{0xff};
|
inline static MemoryChunk unmapped{0xff};
|
||||||
inline static MemoryChunk scratch;
|
inline static MemoryChunk scratch;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PrimarySlot {
|
class PrimarySlot {
|
||||||
public:
|
public:
|
||||||
PrimarySlot(MemorySlotChangeHandler &);
|
PrimarySlot(MemorySlotChangeHandler &);
|
||||||
|
|
||||||
/// @returns A pointer to the area of memory currently underneath @c address that
|
/// @returns A pointer to the area of memory currently underneath @c address that
|
||||||
/// should be read
|
/// should be read
|
||||||
const uint8_t *read_pointer(int segment) const;
|
const uint8_t *read_pointer(int segment) const;
|
||||||
|
|
||||||
/// @returns A pointer to the area of memory currently underneath @c address.
|
/// @returns A pointer to the area of memory currently underneath @c address.
|
||||||
uint8_t *write_pointer(int segment) const;
|
uint8_t *write_pointer(int segment) const;
|
||||||
|
|
||||||
/// Attempts to write the argument as the secondary paging selection.
|
/// Attempts to write the argument as the secondary paging selection.
|
||||||
void set_secondary_paging(uint8_t);
|
void set_secondary_paging(uint8_t);
|
||||||
|
|
||||||
/// @returns The value most recently provided to @c set_secondary_paging.
|
/// @returns The value most recently provided to @c set_secondary_paging.
|
||||||
uint8_t secondary_paging() const;
|
uint8_t secondary_paging() const;
|
||||||
|
|
||||||
/// Indicates whether this slot supports secondary paging.
|
/// Indicates whether this slot supports secondary paging.
|
||||||
bool supports_secondary_paging = false;
|
bool supports_secondary_paging = false;
|
||||||
|
|
||||||
/// Provides the subslot at the specified index.
|
/// Provides the subslot at the specified index.
|
||||||
MemorySlot &subslot(int);
|
MemorySlot &subslot(int);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MemorySlot subslots_[4];
|
MemorySlot subslots_[4];
|
||||||
uint8_t secondary_paging_ = 0;
|
uint8_t secondary_paging_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MemorySlotHandler {
|
class MemorySlotHandler {
|
||||||
public:
|
public:
|
||||||
virtual ~MemorySlotHandler() = default;
|
virtual ~MemorySlotHandler() = default;
|
||||||
|
|
||||||
/*! Advances time by @c half_cycles. */
|
/*! Advances time by @c half_cycles. */
|
||||||
virtual void run_for([[maybe_unused]] HalfCycles half_cycles) {}
|
virtual void run_for([[maybe_unused]] HalfCycles half_cycles) {}
|
||||||
|
|
||||||
/*! Announces an attempt to write @c value to @c address. */
|
/*! 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;
|
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. */
|
/*! 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; }
|
virtual uint8_t read([[maybe_unused]] uint16_t address) { return 0xff; }
|
||||||
|
|
||||||
/*! @returns The probability that this handler is correct for the data it owns. */
|
/*! @returns The probability that this handler is correct for the data it owns. */
|
||||||
float get_confidence() {
|
float get_confidence() {
|
||||||
return confidence_counter_.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 {
|
namespace MasterSystem {
|
||||||
|
|
||||||
class Joystick: public Inputs::ConcreteJoystick {
|
class Joystick: public Inputs::ConcreteJoystick {
|
||||||
public:
|
public:
|
||||||
Joystick() :
|
Joystick() :
|
||||||
ConcreteJoystick({
|
ConcreteJoystick({
|
||||||
Input(Input::Up),
|
Input(Input::Up),
|
||||||
Input(Input::Down),
|
Input(Input::Down),
|
||||||
Input(Input::Left),
|
Input(Input::Left),
|
||||||
Input(Input::Right),
|
Input(Input::Right),
|
||||||
|
|
||||||
Input(Input::Fire, 0),
|
Input(Input::Fire, 0),
|
||||||
Input(Input::Fire, 1)
|
Input(Input::Fire, 1)
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||||
switch(digital_input.type) {
|
switch(digital_input.type) {
|
||||||
default: return;
|
default: return;
|
||||||
|
|
||||||
case Input::Up: if(is_active) state_ &= ~0x01; else state_ |= 0x01; 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::Down: if(is_active) state_ &= ~0x02; else state_ |= 0x02; break;
|
||||||
case Input::Left: if(is_active) state_ &= ~0x04; else state_ |= 0x04; 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::Right: if(is_active) state_ &= ~0x08; else state_ |= 0x08; break;
|
||||||
case Input::Fire:
|
case Input::Fire:
|
||||||
switch(digital_input.info.control.index) {
|
switch(digital_input.info.control.index) {
|
||||||
default: break;
|
default: break;
|
||||||
case 0: if(is_active) state_ &= ~0x10; else state_ |= 0x10; break;
|
case 0: if(is_active) state_ &= ~0x10; else state_ |= 0x10; break;
|
||||||
case 1: if(is_active) state_ &= ~0x20; else state_ |= 0x20; break;
|
case 1: if(is_active) state_ &= ~0x20; else state_ |= 0x20; break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t get_state() {
|
uint8_t get_state() {
|
||||||
return state_;
|
return state_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint8_t state_ = 0xff;
|
uint8_t state_ = 0xff;
|
||||||
};
|
};
|
||||||
|
|
||||||
template <Analyser::Static::Sega::Target::Model model> class ConcreteMachine:
|
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::ScanProducer,
|
||||||
public MachineTypes::TimedMachine {
|
public MachineTypes::TimedMachine {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ConcreteMachine(const Analyser::Static::Sega::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
ConcreteMachine(const Analyser::Static::Sega::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||||
region_(target.region),
|
region_(target.region),
|
||||||
paging_scheme_(target.paging_scheme),
|
paging_scheme_(target.paging_scheme),
|
||||||
z80_(*this),
|
z80_(*this),
|
||||||
sn76489_(
|
sn76489_(
|
||||||
(target.model == Target::Model::SG1000) ? TI::SN76489::Personality::SN76489 : TI::SN76489::Personality::SMS,
|
(target.model == Target::Model::SG1000) ? TI::SN76489::Personality::SN76489 : TI::SN76489::Personality::SMS,
|
||||||
audio_queue_,
|
audio_queue_,
|
||||||
audio_divider),
|
audio_divider),
|
||||||
opll_(audio_queue_, audio_divider),
|
opll_(audio_queue_, audio_divider),
|
||||||
mixer_(sn76489_, opll_),
|
mixer_(sn76489_, opll_),
|
||||||
speaker_(mixer_),
|
speaker_(mixer_),
|
||||||
keyboard_({Inputs::Keyboard::Key::Enter, Inputs::Keyboard::Key::Escape}, {}) {
|
keyboard_({Inputs::Keyboard::Key::Enter, Inputs::Keyboard::Key::Escape}, {}) {
|
||||||
// Pick the clock rate based on the region.
|
// Pick the clock rate based on the region.
|
||||||
const double clock_rate = target.region == Target::Region::Europe ? 3546893.0 : 3579540.0;
|
const double clock_rate = target.region == Target::Region::Europe ? 3546893.0 : 3579540.0;
|
||||||
speaker_.set_input_rate(float(clock_rate / audio_divider));
|
speaker_.set_input_rate(float(clock_rate / audio_divider));
|
||||||
set_clock_rate(clock_rate);
|
set_clock_rate(clock_rate);
|
||||||
|
|
||||||
// Instantiate the joysticks.
|
// Instantiate the joysticks.
|
||||||
joysticks_.emplace_back(new Joystick);
|
joysticks_.emplace_back(new Joystick);
|
||||||
joysticks_.emplace_back(new Joystick);
|
joysticks_.emplace_back(new Joystick);
|
||||||
|
|
||||||
// Clear the memory map.
|
// Clear the memory map.
|
||||||
map(read_pointers_, nullptr, 0x10000, 0);
|
map(read_pointers_, nullptr, 0x10000, 0);
|
||||||
map(write_pointers_, nullptr, 0x10000, 0);
|
map(write_pointers_, nullptr, 0x10000, 0);
|
||||||
|
|
||||||
// Take a copy of the cartridge and place it into memory.
|
// Take a copy of the cartridge and place it into memory.
|
||||||
if(!target.media.cartridges.empty()) {
|
if(!target.media.cartridges.empty()) {
|
||||||
cartridge_ = target.media.cartridges[0]->get_segments()[0].data;
|
cartridge_ = target.media.cartridges[0]->get_segments()[0].data;
|
||||||
}
|
}
|
||||||
if(cartridge_.size() < 48*1024) {
|
if(cartridge_.size() < 48*1024) {
|
||||||
std::size_t new_space = 48*1024 - cartridge_.size();
|
std::size_t new_space = 48*1024 - cartridge_.size();
|
||||||
cartridge_.resize(48*1024);
|
cartridge_.resize(48*1024);
|
||||||
memset(&cartridge_[48*1024 - new_space], 0xff, new_space);
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
~ConcreteMachine() {
|
if(paging_scheme_ == Target::PagingScheme::Codemasters) {
|
||||||
audio_queue_.flush();
|
// 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 {
|
// Load the BIOS if available.
|
||||||
return ChangeEffect::RestartMachine;
|
//
|
||||||
|
// 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 {
|
// Apply a relatively low low-pass filter. More guidance needed here.
|
||||||
vdp_.last_valid()->set_tv_standard(
|
// TODO: this is disabled for now since it isn't applicable for the FM chip, I think.
|
||||||
(region_ == Target::Region::Europe) ?
|
// speaker_.set_high_frequency_cutoff(8000);
|
||||||
TI::TMS::TVStandard::PAL : TI::TMS::TVStandard::NTSC);
|
|
||||||
|
|
||||||
// Doing the following would be technically correct, but isn't
|
// Set default mixer levels: FM off, SN full-throttle.
|
||||||
// especially thread-safe and won't make a substantial difference.
|
set_mixer_levels(0);
|
||||||
// time_until_debounce_ = vdp_->get_time_until_line(-1);
|
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
if(outputs & Output::Audio) {
|
||||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
update_audio();
|
||||||
return vdp_.last_valid()->get_scaled_scan_status();
|
audio_queue_.perform();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||||
vdp_.last_valid()->set_display_type(display_type);
|
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 {
|
if(cycle.is_terminal()) {
|
||||||
return vdp_.last_valid()->get_display_type();
|
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 {
|
case CPU::Z80::PartialMachineCycle::Write:
|
||||||
return &speaker_;
|
if(paging_scheme_ == Target::PagingScheme::Sega) {
|
||||||
}
|
if(address >= 0xfffd && cartridge_.size() > 48*1024) {
|
||||||
|
if(paging_registers_[address - 0xfffd] != *cycle.value) {
|
||||||
void run_for(const Cycles cycles) final {
|
paging_registers_[address - 0xfffd] = *cycle.value;
|
||||||
z80_.run_for(cycles);
|
page_cartridge();
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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;
|
if(write_pointers_[address >> 10]) write_pointers_[address >> 10][address & 1023] = *cycle.value;
|
||||||
// else logger.info().append("Ignored write to ROM");
|
// else logger.info().append("Ignored write to ROM");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CPU::Z80::PartialMachineCycle::Input:
|
case CPU::Z80::PartialMachineCycle::Input:
|
||||||
switch(address & 0xc1) {
|
switch(address & 0xc1) {
|
||||||
case 0x00:
|
case 0x00:
|
||||||
logger.error().append("TODO: [input] memory control");
|
logger.error().append("TODO: [input] memory control");
|
||||||
*cycle.value = 0xff;
|
*cycle.value = 0xff;
|
||||||
break;
|
break;
|
||||||
case 0x01:
|
case 0x01:
|
||||||
logger.error().append("TODO: [input] I/O port control");
|
logger.error().append("TODO: [input] I/O port control");
|
||||||
*cycle.value = 0xff;
|
*cycle.value = 0xff;
|
||||||
break;
|
break;
|
||||||
case 0x40:
|
case 0x40:
|
||||||
*cycle.value = vdp_->get_current_line();
|
*cycle.value = vdp_->get_current_line();
|
||||||
break;
|
break;
|
||||||
case 0x41:
|
case 0x41:
|
||||||
*cycle.value = vdp_.last_valid()->get_latched_horizontal_counter();
|
*cycle.value = vdp_.last_valid()->get_latched_horizontal_counter();
|
||||||
break;
|
break;
|
||||||
case 0x80: case 0x81:
|
case 0x80: case 0x81:
|
||||||
*cycle.value = vdp_->read(address);
|
*cycle.value = vdp_->read(address);
|
||||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||||
break;
|
break;
|
||||||
case 0xc0: {
|
case 0xc0: {
|
||||||
if(memory_control_ & 0x4) {
|
if(memory_control_ & 0x4) {
|
||||||
if(has_fm_audio_ && (address & 0xff) == 0xf2) {
|
if(has_fm_audio_ && (address & 0xff) == 0xf2) {
|
||||||
*cycle.value = opll_detection_word_;
|
*cycle.value = opll_detection_word_;
|
||||||
} else {
|
|
||||||
*cycle.value = 0xff;
|
|
||||||
}
|
|
||||||
} else {
|
} 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;
|
*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:
|
*cycle.value =
|
||||||
logger.error().append("[input] Clearly some sort of typo");
|
(joypad2->get_state() >> 2) |
|
||||||
break;
|
0x30 |
|
||||||
}
|
get_th_values();
|
||||||
break;
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
case CPU::Z80::PartialMachineCycle::Output:
|
default:
|
||||||
switch(address & 0xc1) {
|
logger.error().append("[input] Clearly some sort of typo");
|
||||||
case 0x00: // i.e. even ports less than 0x40.
|
break;
|
||||||
if constexpr (is_master_system(model)) {
|
}
|
||||||
// TODO: Obey the RAM enable.
|
break;
|
||||||
logger.info().append("Memory control: %02x", memory_control_);
|
|
||||||
memory_control_ = *cycle.value;
|
case CPU::Z80::PartialMachineCycle::Output:
|
||||||
page_cartridge();
|
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.
|
break;
|
||||||
// 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.
|
default:
|
||||||
if((new_ths^previous_ths)&new_ths) {
|
logger.error().append("[output] Clearly some sort of typo");
|
||||||
vdp_->latch_horizontal_counter();
|
break;
|
||||||
}
|
}
|
||||||
} break;
|
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;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO: implementation of the below is incomplete.
|
TODO: implementation of the below is incomplete.
|
||||||
@@ -380,181 +380,181 @@ template <Analyser::Static::Sega::Target::Model model> class ConcreteMachine:
|
|||||||
— Charles MacDonald
|
— Charles MacDonald
|
||||||
*/
|
*/
|
||||||
|
|
||||||
case CPU::Z80::PartialMachineCycle::Interrupt:
|
case CPU::Z80::PartialMachineCycle::Interrupt:
|
||||||
*cycle.value = 0xff;
|
*cycle.value = 0xff;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default: 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline uint8_t get_th_values() {
|
// The pause button is debounced and takes effect only one line before pixels
|
||||||
// Quick not on TH inputs here: if either is setup as an output, then the
|
// begin; time_until_debounce_ keeps track of the time until then.
|
||||||
// currently output level is returned. Otherwise they're fixed at 1.
|
time_until_debounce_ -= cycle.length;
|
||||||
return
|
if(time_until_debounce_ <= HalfCycles(0)) {
|
||||||
uint8_t(
|
z80_.set_non_maskable_interrupt_line(pause_is_pressed_);
|
||||||
((io_port_control_ & 0x02) << 5) | ((io_port_control_&0x20) << 1) |
|
time_until_debounce_ = vdp_->get_time_until_line(-1);
|
||||||
((io_port_control_ & 0x08) << 4) | (io_port_control_&0x80)
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void update_audio() {
|
return HalfCycles(0);
|
||||||
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(audio_divider)));
|
}
|
||||||
|
|
||||||
|
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) {
|
if(key == Inputs::Keyboard::Key::Escape) {
|
||||||
// This is as per the audio control register;
|
reset_is_pressed_ = is_pressed;
|
||||||
// see https://www.smspower.org/Development/AudioControlPort
|
return true;
|
||||||
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;
|
return false;
|
||||||
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_;
|
void reset_all_keys(Inputs::Keyboard &) final {
|
||||||
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_;
|
// MARK: - Configuration options.
|
||||||
Inputs::Keyboard keyboard_;
|
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||||
bool reset_is_pressed_ = false, pause_is_pressed_ = false;
|
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||||||
|
options->output = get_video_signal_configurable();
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
HalfCycles time_since_sn76489_update_;
|
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
|
||||||
HalfCycles time_until_debounce_;
|
const auto options = dynamic_cast<Options *>(str.get());
|
||||||
|
set_video_signal_configurable(options->output);
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t ram_[8*1024];
|
private:
|
||||||
uint8_t bios_[8*1024];
|
static constexpr TI::TMS::Personality tms_personality() {
|
||||||
std::vector<uint8_t> cartridge_;
|
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.
|
inline void update_audio() {
|
||||||
const uint8_t *read_pointers_[64];
|
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(audio_divider)));
|
||||||
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;
|
void set_mixer_levels(uint8_t mode) {
|
||||||
for(auto address = start_address; address < end_address; address += 1024) {
|
// This is as per the audio control register;
|
||||||
target[address >> 10] = source ? &source[(address - start_address) & (size - 1)] : nullptr;
|
// 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 paging_registers_[3] = {0, 1, 2};
|
||||||
uint8_t memory_control_ = 0;
|
uint8_t memory_control_ = 0;
|
||||||
void page_cartridge() {
|
void page_cartridge() {
|
||||||
// Either install the cartridge or don't; Japanese machines can't see
|
// Either install the cartridge or don't; Japanese machines can't see
|
||||||
// anything but the cartridge.
|
// anything but the cartridge.
|
||||||
if(!(memory_control_ & 0x40) || region_ == Target::Region::Japan) {
|
if(!(memory_control_ & 0x40) || region_ == Target::Region::Japan) {
|
||||||
for(size_t c = 0; c < 3; ++c) {
|
for(size_t c = 0; c < 3; ++c) {
|
||||||
const size_t start_addr = (paging_registers_[c] * 0x4000) % cartridge_.size();
|
const size_t start_addr = (paging_registers_[c] * 0x4000) % cartridge_.size();
|
||||||
map(
|
map(
|
||||||
read_pointers_,
|
read_pointers_,
|
||||||
cartridge_.data() + start_addr,
|
cartridge_.data() + start_addr,
|
||||||
std::min(size_t(0x4000), cartridge_.size() - start_addr),
|
std::min(size_t(0x4000), cartridge_.size() - start_addr),
|
||||||
c * 0x4000);
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throw the BIOS on top if this machine has one and it isn't disabled.
|
// The first 1kb doesn't page though, if this is the Sega paging scheme.
|
||||||
if(has_bios_ && !(memory_control_ & 0x08)) {
|
if(paging_scheme_ == Target::PagingScheme::Sega) {
|
||||||
map(read_pointers_, bios_, 8*1024, 0);
|
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
|
see the thread at https://forum.defence-force.org/viewtopic.php?f=25&t=2055
|
||||||
*/
|
*/
|
||||||
class BD500: public DiskController {
|
class BD500: public DiskController {
|
||||||
public:
|
public:
|
||||||
BD500();
|
BD500();
|
||||||
|
|
||||||
void write(int address, uint8_t value);
|
void write(int address, uint8_t value);
|
||||||
uint8_t read(int address);
|
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:
|
private:
|
||||||
void set_head_load_request(bool head_load) final;
|
void set_head_load_request(bool head_load) final;
|
||||||
bool is_loading_head_ = false;
|
bool is_loading_head_ = false;
|
||||||
Activity::Observer *observer_ = nullptr;
|
Activity::Observer *observer_ = nullptr;
|
||||||
|
|
||||||
void access(int address);
|
void access(int address);
|
||||||
void set_head_loaded(bool loaded);
|
void set_head_loaded(bool loaded);
|
||||||
|
|
||||||
bool enable_overlay_ram_ = false;
|
bool enable_overlay_ram_ = false;
|
||||||
bool disable_basic_rom_ = false;
|
bool disable_basic_rom_ = false;
|
||||||
void select_paged_item() {
|
void select_paged_item() {
|
||||||
PagedItem item = PagedItem::RAM;
|
PagedItem item = PagedItem::RAM;
|
||||||
if(!enable_overlay_ram_) {
|
if(!enable_overlay_ram_) {
|
||||||
item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC;
|
item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC;
|
||||||
}
|
|
||||||
set_paged_item(item);
|
|
||||||
}
|
}
|
||||||
|
set_paged_item(item);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -11,50 +11,50 @@
|
|||||||
namespace Oric {
|
namespace Oric {
|
||||||
|
|
||||||
class DiskController: public WD::WD1770 {
|
class DiskController: public WD::WD1770 {
|
||||||
public:
|
public:
|
||||||
DiskController(WD::WD1770::Personality personality, int clock_rate, Storage::Disk::Drive::ReadyType ready_type) :
|
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) {
|
WD::WD1770(personality), clock_rate_(clock_rate), ready_type_(ready_type) {
|
||||||
emplace_drives(4, clock_rate_, 300, 2, ready_type_);
|
emplace_drives(4, clock_rate_, 300, 2, ready_type_);
|
||||||
// TODO: don't assume four drives?
|
// 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) {
|
private:
|
||||||
get_drive(size_t(d)).set_disk(disk);
|
PagedItem paged_item_ = PagedItem::DiskROM;
|
||||||
}
|
int clock_rate_;
|
||||||
|
Storage::Disk::Drive::ReadyType ready_type_;
|
||||||
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_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -15,29 +15,29 @@
|
|||||||
namespace Oric {
|
namespace Oric {
|
||||||
|
|
||||||
class Jasmin: public DiskController {
|
class Jasmin: public DiskController {
|
||||||
public:
|
public:
|
||||||
Jasmin();
|
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:
|
private:
|
||||||
void set_motor_on(bool on) final;
|
void set_motor_on(bool on) final;
|
||||||
bool motor_on_ = false;
|
bool motor_on_ = false;
|
||||||
uint8_t selected_drives_ = 0;
|
uint8_t selected_drives_ = 0;
|
||||||
|
|
||||||
Activity::Observer *observer_ = nullptr;
|
Activity::Observer *observer_ = nullptr;
|
||||||
|
|
||||||
bool enable_overlay_ram_ = false;
|
bool enable_overlay_ram_ = false;
|
||||||
bool disable_basic_rom_ = false;
|
bool disable_basic_rom_ = false;
|
||||||
void select_paged_item() {
|
void select_paged_item() {
|
||||||
PagedItem item = PagedItem::RAM;
|
PagedItem item = PagedItem::RAM;
|
||||||
if(!enable_overlay_ram_) {
|
if(!enable_overlay_ram_) {
|
||||||
item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC;
|
item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC;
|
||||||
}
|
|
||||||
set_paged_item(item);
|
|
||||||
}
|
}
|
||||||
|
set_paged_item(item);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -15,30 +15,30 @@
|
|||||||
namespace Oric {
|
namespace Oric {
|
||||||
|
|
||||||
class Microdisc: public DiskController {
|
class Microdisc: public DiskController {
|
||||||
public:
|
public:
|
||||||
Microdisc();
|
Microdisc();
|
||||||
|
|
||||||
void set_control_register(uint8_t control);
|
void set_control_register(uint8_t control);
|
||||||
uint8_t get_interrupt_request_register();
|
uint8_t get_interrupt_request_register();
|
||||||
uint8_t get_data_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:
|
private:
|
||||||
void set_head_load_request(bool head_load) final;
|
void set_head_load_request(bool head_load) final;
|
||||||
|
|
||||||
void set_control_register(uint8_t control, uint8_t changes);
|
void set_control_register(uint8_t control, uint8_t changes);
|
||||||
uint8_t last_control_ = 0;
|
uint8_t last_control_ = 0;
|
||||||
bool irq_enable_ = false;
|
bool irq_enable_ = false;
|
||||||
|
|
||||||
Cycles::IntType head_load_request_counter_ = -1;
|
Cycles::IntType head_load_request_counter_ = -1;
|
||||||
bool head_load_request_ = false;
|
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 {
|
namespace Oric {
|
||||||
|
|
||||||
class VideoOutput {
|
class VideoOutput {
|
||||||
public:
|
public:
|
||||||
VideoOutput(uint8_t *memory);
|
VideoOutput(uint8_t *memory);
|
||||||
void set_colour_rom(const std::vector<uint8_t> &colour_rom);
|
void set_colour_rom(const std::vector<uint8_t> &colour_rom);
|
||||||
|
|
||||||
void run_for(const Cycles cycles);
|
void run_for(const Cycles cycles);
|
||||||
Cycles next_sequence_point() const;
|
Cycles next_sequence_point() const;
|
||||||
|
|
||||||
bool vsync();
|
bool vsync();
|
||||||
|
|
||||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||||
void set_display_type(Outputs::Display::DisplayType display_type);
|
void set_display_type(Outputs::Display::DisplayType display_type);
|
||||||
Outputs::Display::DisplayType get_display_type() const;
|
Outputs::Display::DisplayType get_display_type() const;
|
||||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||||
|
|
||||||
void register_crt_frequency_mismatch();
|
void register_crt_frequency_mismatch();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint8_t *ram_;
|
uint8_t *ram_;
|
||||||
Outputs::CRT::CRT crt_;
|
Outputs::CRT::CRT crt_;
|
||||||
Outputs::CRT::CRTFrequencyMismatchWarner<VideoOutput> frequency_mismatch_warner_;
|
Outputs::CRT::CRTFrequencyMismatchWarner<VideoOutput> frequency_mismatch_warner_;
|
||||||
bool crt_is_60Hz_ = false;
|
bool crt_is_60Hz_ = false;
|
||||||
bool has_colour_rom_ = false;
|
bool has_colour_rom_ = false;
|
||||||
|
|
||||||
void update_crt_frequency();
|
void update_crt_frequency();
|
||||||
|
|
||||||
// Counters and limits.
|
// Counters and limits.
|
||||||
int counter_ = 0, frame_counter_ = 0;
|
int counter_ = 0, frame_counter_ = 0;
|
||||||
int v_sync_start_position_, v_sync_end_position_, counter_period_;
|
int v_sync_start_position_, v_sync_end_position_, counter_period_;
|
||||||
|
|
||||||
// Output target and device.
|
// Output target and device.
|
||||||
uint8_t *rgb_pixel_target_ = nullptr;
|
uint8_t *rgb_pixel_target_ = nullptr;
|
||||||
uint32_t *composite_pixel_target_ = nullptr;
|
uint32_t *composite_pixel_target_ = nullptr;
|
||||||
uint32_t colour_forms_[8];
|
uint32_t colour_forms_[8];
|
||||||
Outputs::Display::InputDataType data_type_;
|
Outputs::Display::InputDataType data_type_;
|
||||||
|
|
||||||
// Registers.
|
// Registers.
|
||||||
uint8_t ink_, paper_;
|
uint8_t ink_, paper_;
|
||||||
|
|
||||||
int character_set_base_address_ = 0xb400;
|
int character_set_base_address_ = 0xb400;
|
||||||
inline void set_character_set_base_address();
|
inline void set_character_set_base_address();
|
||||||
|
|
||||||
bool is_graphics_mode_ = false;
|
bool is_graphics_mode_ = false;
|
||||||
bool next_frame_is_sixty_hertz_ = false;
|
bool next_frame_is_sixty_hertz_ = false;
|
||||||
bool use_alternative_character_set_;
|
bool use_alternative_character_set_;
|
||||||
bool use_double_height_characters_;
|
bool use_double_height_characters_;
|
||||||
bool blink_text_;
|
bool blink_text_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -37,38 +37,38 @@ enum Key: uint16_t {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class Keyboard {
|
class Keyboard {
|
||||||
public:
|
public:
|
||||||
Keyboard(Machine machine);
|
Keyboard(Machine machine);
|
||||||
|
|
||||||
void set_key_state(uint16_t key, bool is_pressed);
|
void set_key_state(uint16_t key, bool is_pressed);
|
||||||
void clear_all_keys();
|
void clear_all_keys();
|
||||||
|
|
||||||
uint8_t read(uint16_t address);
|
uint8_t read(uint16_t address);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint8_t key_states_[8];
|
uint8_t key_states_[8];
|
||||||
const Machine machine_;
|
const Machine machine_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||||
public:
|
public:
|
||||||
KeyboardMapper(Machine machine);
|
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:
|
private:
|
||||||
const Machine machine_;
|
const Machine machine_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CharacterMapper: public ::Utility::CharacterMapper {
|
class CharacterMapper: public ::Utility::CharacterMapper {
|
||||||
public:
|
public:
|
||||||
CharacterMapper(Machine machine);
|
CharacterMapper(Machine machine);
|
||||||
const uint16_t *sequence_for_character(char character) const override;
|
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:
|
private:
|
||||||
const Machine machine_;
|
const Machine machine_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -24,36 +24,36 @@ namespace Sinclair::ZX8081 {
|
|||||||
and the black level.
|
and the black level.
|
||||||
*/
|
*/
|
||||||
class Video {
|
class Video {
|
||||||
public:
|
public:
|
||||||
/// Constructs an instance of the video feed.
|
/// Constructs an instance of the video feed.
|
||||||
Video();
|
Video();
|
||||||
|
|
||||||
/// Advances time by @c half-cycles.
|
/// Advances time by @c half-cycles.
|
||||||
void run_for(const HalfCycles);
|
void run_for(const HalfCycles);
|
||||||
|
|
||||||
/// Forces output to catch up to the current output position.
|
/// Forces output to catch up to the current output position.
|
||||||
void flush();
|
void flush();
|
||||||
|
|
||||||
/// Sets the current sync output.
|
/// Sets the current sync output.
|
||||||
void set_sync(bool sync);
|
void set_sync(bool sync);
|
||||||
|
|
||||||
/// Causes @c byte to be serialised into pixels and output over the next four cycles.
|
/// Causes @c byte to be serialised into pixels and output over the next four cycles.
|
||||||
void output_byte(uint8_t byte);
|
void output_byte(uint8_t byte);
|
||||||
|
|
||||||
/// Sets the scan target.
|
/// Sets the scan target.
|
||||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||||
|
|
||||||
/// Gets the current scan status.
|
/// Gets the current scan status.
|
||||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool sync_ = false;
|
bool sync_ = false;
|
||||||
uint8_t *line_data_ = nullptr;
|
uint8_t *line_data_ = nullptr;
|
||||||
uint8_t *line_data_pointer_ = nullptr;
|
uint8_t *line_data_pointer_ = nullptr;
|
||||||
HalfCycles time_since_update_ = 0;
|
HalfCycles time_since_update_ = 0;
|
||||||
Outputs::CRT::CRT crt_;
|
Outputs::CRT::CRT crt_;
|
||||||
|
|
||||||
void flush(bool next_sync);
|
void flush(bool next_sync);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -61,433 +61,433 @@ template<bool is_zx81> class ConcreteMachine:
|
|||||||
public Utility::TypeRecipient<CharacterMapper>,
|
public Utility::TypeRecipient<CharacterMapper>,
|
||||||
public CPU::Z80::BusHandler,
|
public CPU::Z80::BusHandler,
|
||||||
public Machine {
|
public Machine {
|
||||||
public:
|
public:
|
||||||
ConcreteMachine(const Analyser::Static::ZX8081::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
ConcreteMachine(const Analyser::Static::ZX8081::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||||
Utility::TypeRecipient<CharacterMapper>(keyboard_machine()),
|
Utility::TypeRecipient<CharacterMapper>(keyboard_machine()),
|
||||||
z80_(*this),
|
z80_(*this),
|
||||||
keyboard_(keyboard_machine()),
|
keyboard_(keyboard_machine()),
|
||||||
keyboard_mapper_(keyboard_machine()),
|
keyboard_mapper_(keyboard_machine()),
|
||||||
tape_player_(ZX8081ClockRate),
|
tape_player_(ZX8081ClockRate),
|
||||||
ay_(GI::AY38910::Personality::AY38910, audio_queue_),
|
ay_(GI::AY38910::Personality::AY38910, audio_queue_),
|
||||||
speaker_(ay_) {
|
speaker_(ay_) {
|
||||||
set_clock_rate(ZX8081ClockRate);
|
set_clock_rate(ZX8081ClockRate);
|
||||||
speaker_.set_input_rate(float(ZX8081ClockRate) / 2.0f);
|
speaker_.set_input_rate(float(ZX8081ClockRate) / 2.0f);
|
||||||
|
|
||||||
const bool use_zx81_rom = target.is_ZX81 || target.ZX80_uses_ZX81_ROM;
|
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::Name rom_name = use_zx81_rom ? ROM::Name::ZX81 : ROM::Name::ZX80;
|
||||||
const ROM::Request request(rom_name);
|
const ROM::Request request(rom_name);
|
||||||
auto roms = rom_fetcher(request);
|
auto roms = rom_fetcher(request);
|
||||||
if(!request.validate(roms)) {
|
if(!request.validate(roms)) {
|
||||||
throw ROMMachine::Error::MissingROMs;
|
throw ROMMachine::Error::MissingROMs;
|
||||||
}
|
}
|
||||||
rom_ = std::move(roms.find(rom_name)->second);
|
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) {
|
switch(target.memory_model) {
|
||||||
case Analyser::Static::ZX8081::Target::MemoryModel::Unexpanded:
|
case Analyser::Static::ZX8081::Target::MemoryModel::Unexpanded:
|
||||||
ram_.resize(1024);
|
ram_.resize(1024);
|
||||||
ram_base_ = 16384;
|
ram_base_ = 16384;
|
||||||
ram_mask_ = 1023;
|
ram_mask_ = 1023;
|
||||||
break;
|
break;
|
||||||
case Analyser::Static::ZX8081::Target::MemoryModel::SixteenKB:
|
case Analyser::Static::ZX8081::Target::MemoryModel::SixteenKB:
|
||||||
ram_.resize(16384);
|
ram_.resize(16384);
|
||||||
ram_base_ = 16384;
|
ram_base_ = 16384;
|
||||||
ram_mask_ = 16383;
|
ram_mask_ = 16383;
|
||||||
break;
|
break;
|
||||||
case Analyser::Static::ZX8081::Target::MemoryModel::SixtyFourKB:
|
case Analyser::Static::ZX8081::Target::MemoryModel::SixtyFourKB:
|
||||||
ram_.resize(65536);
|
ram_.resize(65536);
|
||||||
ram_base_ = 8192;
|
ram_base_ = 8192;
|
||||||
ram_mask_ = 65535;
|
ram_mask_ = 65535;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Memory::Fuzz(ram_);
|
Memory::Fuzz(ram_);
|
||||||
|
|
||||||
// Ensure valid initial key state.
|
// Ensure valid initial key state.
|
||||||
clear_all_keys();
|
clear_all_keys();
|
||||||
if(!target.loading_command.empty()) {
|
if(!target.loading_command.empty()) {
|
||||||
type_string(target.loading_command);
|
type_string(target.loading_command);
|
||||||
should_autorun_ = true;
|
should_autorun_ = true;
|
||||||
}
|
|
||||||
|
|
||||||
insert_media(target.media);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
~ConcreteMachine() {
|
insert_media(target.media);
|
||||||
audio_queue_.flush();
|
}
|
||||||
|
|
||||||
|
~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) {
|
if constexpr (is_zx81) horizontal_counter_ %= HalfCycles(Cycles(207));
|
||||||
const HalfCycles previous_counter = horizontal_counter_;
|
if(!tape_advance_delay_) {
|
||||||
horizontal_counter_ += cycle.length;
|
tape_player_.run_for(cycle.length);
|
||||||
time_since_ay_update_ += cycle.length;
|
} else {
|
||||||
|
tape_advance_delay_ = std::max(tape_advance_delay_ - cycle.length, HalfCycles(0));
|
||||||
|
}
|
||||||
|
|
||||||
if(previous_counter < vsync_start_ && horizontal_counter_ >= vsync_start_) {
|
if(nmi_is_enabled_ && !z80_.get_halt_line() && z80_.get_non_maskable_interrupt_line()) {
|
||||||
video_.run_for(vsync_start_ - previous_counter);
|
z80_.set_wait_line(true);
|
||||||
set_hsync(true);
|
}
|
||||||
line_counter_ = (line_counter_ + 1) & 7;
|
|
||||||
if(nmi_is_enabled_) {
|
if(!cycle.is_terminal()) {
|
||||||
z80_.set_non_maskable_interrupt_line(true);
|
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_);
|
if(!(address & 2)) nmi_is_enabled_ = false;
|
||||||
} else if(previous_counter < vsync_end_ && horizontal_counter_ >= vsync_end_) {
|
if(!(address & 1)) nmi_is_enabled_ = is_zx81;
|
||||||
video_.run_for(vsync_end_ - previous_counter);
|
if(is_zx81 && !nmi_is_enabled_) z80_.set_wait_line(false);
|
||||||
set_hsync(false);
|
|
||||||
if(nmi_is_enabled_) {
|
// The below emulates the ZonX AY expansion device.
|
||||||
z80_.set_non_maskable_interrupt_line(false);
|
if constexpr (is_zx81) {
|
||||||
z80_.set_wait_line(false);
|
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_);
|
break;
|
||||||
} else {
|
|
||||||
video_.run_for(cycle.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if constexpr (is_zx81) horizontal_counter_ %= HalfCycles(Cycles(207));
|
case CPU::Z80::PartialMachineCycle::Input: {
|
||||||
if(!tape_advance_delay_) {
|
uint8_t value = 0xff;
|
||||||
tape_player_.run_for(cycle.length);
|
if(!(address&1)) {
|
||||||
} else {
|
if(!nmi_is_enabled_) set_vsync(true);
|
||||||
tape_advance_delay_ = std::max(tape_advance_delay_ - cycle.length, HalfCycles(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(nmi_is_enabled_ && !z80_.get_halt_line() && z80_.get_non_maskable_interrupt_line()) {
|
value &= keyboard_.read(address);
|
||||||
z80_.set_wait_line(true);
|
value &= ~(tape_player_.input() ? 0x00 : 0x80);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!cycle.is_terminal()) {
|
// The below emulates the ZonX AY expansion device.
|
||||||
return Cycles(0);
|
if constexpr (is_zx81) {
|
||||||
}
|
if((address&0xef) == 0xcf) {
|
||||||
|
value &= ay_read_data();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
if(!(address & 2)) nmi_is_enabled_ = false;
|
}
|
||||||
if(!(address & 1)) nmi_is_enabled_ = is_zx81;
|
*cycle.value = value;
|
||||||
if(is_zx81 && !nmi_is_enabled_) z80_.set_wait_line(false);
|
} break;
|
||||||
|
|
||||||
// The below emulates the ZonX AY expansion device.
|
case CPU::Z80::PartialMachineCycle::Interrupt:
|
||||||
if constexpr (is_zx81) {
|
// resetting event is M1 and IOREQ both simultaneously having leading edges;
|
||||||
if((address&0xef) == 0xcf) {
|
// that happens 2 cycles before the end of INTACK. So the timer was reset and
|
||||||
ay_set_register(*cycle.value);
|
// now has advanced twice.
|
||||||
} else if((address&0xef) == 0x0f) {
|
horizontal_counter_ = HalfCycles(2);
|
||||||
ay_set_data(*cycle.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CPU::Z80::PartialMachineCycle::Input: {
|
*cycle.value = 0xff;
|
||||||
uint8_t value = 0xff;
|
break;
|
||||||
if(!(address&1)) {
|
|
||||||
if(!nmi_is_enabled_) set_vsync(true);
|
|
||||||
|
|
||||||
value &= keyboard_.read(address);
|
case CPU::Z80::PartialMachineCycle::Refresh:
|
||||||
value &= ~(tape_player_.input() ? 0x00 : 0x80);
|
// 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
|
||||||
// The below emulates the ZonX AY expansion device.
|
// of the IRQ line if necessary.
|
||||||
if constexpr (is_zx81) {
|
if(!(address & 0x40)) {
|
||||||
if((address&0xef) == 0xcf) {
|
z80_.set_interrupt_line(true, Cycles(-2));
|
||||||
value &= ay_read_data();
|
z80_.set_interrupt_line(false);
|
||||||
}
|
}
|
||||||
}
|
if(has_latched_video_byte_) {
|
||||||
*cycle.value = value;
|
std::size_t char_address = size_t((address & 0xfe00) | ((latched_video_byte_ & 0x3f) << 3) | line_counter_);
|
||||||
} break;
|
const uint8_t mask = (latched_video_byte_ & 0x80) ? 0x00 : 0xff;
|
||||||
|
if(char_address < ram_base_) {
|
||||||
case CPU::Z80::PartialMachineCycle::Interrupt:
|
latched_video_byte_ = rom_[char_address & rom_mask_] ^ mask;
|
||||||
// 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_];
|
|
||||||
} else {
|
} else {
|
||||||
const uint8_t value = ram_[address & ram_mask_];
|
latched_video_byte_ = ram_[address & ram_mask_] ^ 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:
|
video_.output_byte(latched_video_byte_);
|
||||||
if(address >= ram_base_) {
|
has_latched_video_byte_ = false;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
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 {
|
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||||
video_.set_scan_target(scan_target);
|
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 {
|
set_use_fast_tape();
|
||||||
return video_.get_scaled_scan_status();
|
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 {
|
void set_tape_is_playing(bool is_playing) final {
|
||||||
return is_zx81 ? &speaker_ : nullptr;
|
tape_player_.set_motor_control(is_playing);
|
||||||
}
|
}
|
||||||
|
|
||||||
void run_for(const Cycles cycles) final {
|
bool get_tape_is_playing() final {
|
||||||
z80_.run_for(cycles);
|
return tape_player_.motor_control();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool insert_media(const Analyser::Static::Media &media) final {
|
// MARK: - Typer timing
|
||||||
if(!media.tapes.empty()) {
|
HalfCycles get_typer_delay(const std::string &) const final {
|
||||||
tape_player_.set_tape(media.tapes.front(), is_zx81 ? TargetPlatform::ZX81 : TargetPlatform::ZX80);
|
return z80_.get_is_resetting() ? Cycles(7'000'000) : Cycles(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
set_use_fast_tape();
|
HalfCycles get_typer_frequency() const final {
|
||||||
return !media.tapes.empty();
|
return Cycles(159'961);
|
||||||
}
|
}
|
||||||
|
|
||||||
void type_string(const std::string &string) final {
|
KeyboardMapper *get_keyboard_mapper() final {
|
||||||
Utility::TypeRecipient<CharacterMapper>::add_typer(string);
|
return &keyboard_mapper_;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool can_type(char c) const final {
|
// MARK: - Configuration options.
|
||||||
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Keyboard
|
std::unique_ptr<Reflection::Struct> get_options() const final {
|
||||||
void set_key_state(uint16_t key, bool is_pressed) final {
|
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly); // OptionsType is arbitrary, but not optional.
|
||||||
keyboard_.set_key_state(key, is_pressed);
|
options->automatic_tape_motor_control = use_automatic_tape_motor_control_;
|
||||||
}
|
options->quickload = allow_fast_tape_hack_;
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
void clear_all_keys() final {
|
void set_options(const std::unique_ptr<Reflection::Struct> &str) {
|
||||||
keyboard_.clear_all_keys();
|
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() {
|
private:
|
||||||
return is_zx81 ? Sinclair::ZX::Keyboard::Machine::ZX81 : Sinclair::ZX::Keyboard::Machine::ZX80;
|
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) {
|
// If automatic tape motor control is enabled then the tape will be permitted to play any time
|
||||||
use_automatic_tape_motor_control_ = enabled;
|
// the program counter is >= automatic_tape_motor_start_address_ and < automatic_tape_motor_end_address_.
|
||||||
if(!enabled) {
|
static constexpr uint16_t automatic_tape_motor_start_address_ = is_zx81 ? 0x340 : 0x206;
|
||||||
tape_player_.set_motor_control(false);
|
static constexpr uint16_t automatic_tape_motor_end_address_ = is_zx81 ? 0x3c3 : 0x24d;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void set_tape_is_playing(bool is_playing) final {
|
// When automatically loading, if the PC gets to the finished_load_address_ in order to print 0/0
|
||||||
tape_player_.set_motor_control(is_playing);
|
// (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 {
|
std::vector<uint8_t> ram_;
|
||||||
return tape_player_.motor_control();
|
uint16_t ram_mask_, ram_base_;
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Typer timing
|
std::vector<uint8_t> rom_;
|
||||||
HalfCycles get_typer_delay(const std::string &) const final {
|
uint16_t rom_mask_;
|
||||||
return z80_.get_is_resetting() ? Cycles(7'000'000) : Cycles(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
HalfCycles get_typer_frequency() const final {
|
bool vsync_ = false, hsync_ = false;
|
||||||
return Cycles(159'961);
|
int line_counter_ = 0;
|
||||||
}
|
|
||||||
|
|
||||||
KeyboardMapper *get_keyboard_mapper() final {
|
Sinclair::ZX::Keyboard::Keyboard keyboard_;
|
||||||
return &keyboard_mapper_;
|
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 {
|
bool nmi_is_enabled_ = false;
|
||||||
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 set_options(const std::unique_ptr<Reflection::Struct> &str) {
|
static constexpr auto vsync_start_ = is_zx81 ? HalfCycles(32) : HalfCycles(26);
|
||||||
const auto options = dynamic_cast<Options *>(str.get());
|
static constexpr auto vsync_end_ = is_zx81 ? HalfCycles(64) : HalfCycles(66);
|
||||||
set_use_automatic_tape_motor_control(options->automatic_tape_motor_control);
|
HalfCycles horizontal_counter_;
|
||||||
allow_fast_tape_hack_ = options->quickload;
|
|
||||||
set_use_fast_tape();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
uint8_t latched_video_byte_ = 0;
|
||||||
CPU::Z80::Processor<ConcreteMachine, false, is_zx81> z80_;
|
bool has_latched_video_byte_ = false;
|
||||||
Video video_;
|
|
||||||
|
|
||||||
// If fast tape loading is enabled then the PC will be trapped at tape_trap_address_;
|
bool use_fast_tape_hack_ = false;
|
||||||
// the emulator will then do a high-level reinterpretation of the standard ZX80/81 reading
|
bool allow_fast_tape_hack_ = false;
|
||||||
// of a single byte, and the next thing executed will be at tape_return_address_;
|
void set_use_fast_tape() {
|
||||||
static constexpr uint16_t tape_trap_address_ = is_zx81 ? 0x37c : 0x220;
|
use_fast_tape_hack_ = allow_fast_tape_hack_ && tape_player_.has_tape();
|
||||||
static constexpr uint16_t tape_return_address_ = is_zx81 ? 0x380 : 0x248;
|
}
|
||||||
|
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
|
// MARK: - Video
|
||||||
// the program counter is >= automatic_tape_motor_start_address_ and < automatic_tape_motor_end_address_.
|
inline void set_vsync(bool sync) {
|
||||||
static constexpr uint16_t automatic_tape_motor_start_address_ = is_zx81 ? 0x340 : 0x206;
|
vsync_ = sync;
|
||||||
static constexpr uint16_t automatic_tape_motor_end_address_ = is_zx81 ? 0x3c3 : 0x24d;
|
update_sync();
|
||||||
|
}
|
||||||
|
|
||||||
// When automatically loading, if the PC gets to the finished_load_address_ in order to print 0/0
|
inline void set_hsync(bool sync) {
|
||||||
// (so it's anything that indicates that loading completed, but the program did not autorun) then the
|
hsync_ = sync;
|
||||||
// emulator will automatically RUN whatever has been loaded.
|
update_sync();
|
||||||
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;
|
|
||||||
|
|
||||||
std::vector<uint8_t> ram_;
|
inline void update_sync() {
|
||||||
uint16_t ram_mask_, ram_base_;
|
video_.set_sync(vsync_ || hsync_);
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> rom_;
|
// MARK: - Audio
|
||||||
uint16_t rom_mask_;
|
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||||
|
using AY = GI::AY38910::AY38910<false>;
|
||||||
bool vsync_ = false, hsync_ = false;
|
AY ay_;
|
||||||
int line_counter_ = 0;
|
Outputs::Speaker::PullLowpass<AY> speaker_;
|
||||||
|
HalfCycles time_since_ay_update_;
|
||||||
Sinclair::ZX::Keyboard::Keyboard keyboard_;
|
inline void ay_set_register(uint8_t value) {
|
||||||
Sinclair::ZX::Keyboard::KeyboardMapper keyboard_mapper_;
|
update_audio();
|
||||||
|
GI::AY38910::Utility::select_register(ay_, value);
|
||||||
HalfClockReceiver<Storage::Tape::BinaryTapePlayer> tape_player_;
|
}
|
||||||
Storage::Tape::ZX8081::Parser parser_;
|
inline void ay_set_data(uint8_t value) {
|
||||||
|
update_audio();
|
||||||
bool nmi_is_enabled_ = false;
|
GI::AY38910::Utility::write_data(ay_, value);
|
||||||
|
}
|
||||||
static constexpr auto vsync_start_ = is_zx81 ? HalfCycles(32) : HalfCycles(26);
|
inline uint8_t ay_read_data() {
|
||||||
static constexpr auto vsync_end_ = is_zx81 ? HalfCycles(64) : HalfCycles(66);
|
update_audio();
|
||||||
HalfCycles horizontal_counter_;
|
return GI::AY38910::Utility::read(ay_);
|
||||||
|
}
|
||||||
uint8_t latched_video_byte_ = 0;
|
inline void update_audio() {
|
||||||
bool has_latched_video_byte_ = false;
|
speaker_.run_for(audio_queue_, time_since_ay_update_.divide_cycles(Cycles(2)));
|
||||||
|
}
|
||||||
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)));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -48,134 +48,134 @@ enum class Timing {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
template <Timing timing> class Video {
|
template <Timing timing> class Video {
|
||||||
private:
|
private:
|
||||||
struct Timings {
|
struct Timings {
|
||||||
// Number of cycles per line. Will be 224 or 228.
|
// Number of cycles per line. Will be 224 or 228.
|
||||||
int half_cycles_per_line;
|
int half_cycles_per_line;
|
||||||
// Number of lines comprising a whole frame. Will be 311 or 312.
|
// Number of lines comprising a whole frame. Will be 311 or 312.
|
||||||
int lines_per_frame;
|
int lines_per_frame;
|
||||||
|
|
||||||
// Number of cycles before first pixel fetch that contention starts to be applied.
|
// Number of cycles before first pixel fetch that contention starts to be applied.
|
||||||
int contention_leadin;
|
int contention_leadin;
|
||||||
// Period in a line for which contention is applied.
|
// Period in a line for which contention is applied.
|
||||||
int contention_duration;
|
int contention_duration;
|
||||||
|
|
||||||
// Number of cycles after first pixel fetch at which interrupt is first signalled.
|
// Number of cycles after first pixel fetch at which interrupt is first signalled.
|
||||||
int interrupt_time;
|
int interrupt_time;
|
||||||
|
|
||||||
// Contention to apply, in whole cycles, as a function of number of whole cycles since
|
// Contention to apply, in whole cycles, as a function of number of whole cycles since
|
||||||
// contention began.
|
// contention began.
|
||||||
int delays[8];
|
int delays[8];
|
||||||
|
|
||||||
constexpr Timings(
|
constexpr Timings(
|
||||||
const int cycles_per_line,
|
const int cycles_per_line,
|
||||||
const int lines_per_frame,
|
const int lines_per_frame,
|
||||||
const int contention_leadin,
|
const int contention_leadin,
|
||||||
const int contention_duration,
|
const int contention_duration,
|
||||||
const int interrupt_offset,
|
const int interrupt_offset,
|
||||||
const int (&delays)[8])
|
const int (&delays)[8])
|
||||||
noexcept :
|
noexcept :
|
||||||
half_cycles_per_line(cycles_per_line * 2),
|
half_cycles_per_line(cycles_per_line * 2),
|
||||||
lines_per_frame(lines_per_frame),
|
lines_per_frame(lines_per_frame),
|
||||||
contention_leadin(contention_leadin * 2),
|
contention_leadin(contention_leadin * 2),
|
||||||
contention_duration(contention_duration * 2),
|
contention_duration(contention_duration * 2),
|
||||||
interrupt_time((cycles_per_line * lines_per_frame - interrupt_offset - contention_leadin) * 2),
|
interrupt_time((cycles_per_line * lines_per_frame - interrupt_offset - contention_leadin) * 2),
|
||||||
delays{
|
delays{
|
||||||
delays[0] * 2,
|
delays[0] * 2,
|
||||||
delays[1] * 2,
|
delays[1] * 2,
|
||||||
delays[2] * 2,
|
delays[2] * 2,
|
||||||
delays[3] * 2,
|
delays[3] * 2,
|
||||||
delays[4] * 2,
|
delays[4] * 2,
|
||||||
delays[5] * 2,
|
delays[5] * 2,
|
||||||
delays[6] * 2,
|
delays[6] * 2,
|
||||||
delays[7] * 2
|
delays[7] * 2
|
||||||
}
|
|
||||||
{}
|
|
||||||
};
|
|
||||||
|
|
||||||
static constexpr Timings get_timings() {
|
|
||||||
if constexpr (timing == Timing::Plus3) {
|
|
||||||
constexpr int delays[] = {1, 0, 7, 6, 5, 4, 3, 2};
|
|
||||||
return Timings(228, 311, 6, 129, 14361, delays);
|
|
||||||
}
|
}
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
if constexpr (timing == Timing::OneTwoEightK) {
|
static constexpr Timings get_timings() {
|
||||||
constexpr int delays[] = {6, 5, 4, 3, 2, 1, 0, 0};
|
if constexpr (timing == Timing::Plus3) {
|
||||||
return Timings(228, 311, 4, 128, 14361, delays);
|
constexpr int delays[] = {1, 0, 7, 6, 5, 4, 3, 2};
|
||||||
}
|
return Timings(228, 311, 6, 129, 14361, delays);
|
||||||
|
|
||||||
if constexpr (timing == Timing::FortyEightK) {
|
|
||||||
constexpr int delays[] = {6, 5, 4, 3, 2, 1, 0, 0};
|
|
||||||
return Timings(224, 312, 4, 128, 14335, delays);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interrupt should be held for 32 cycles.
|
if constexpr (timing == Timing::OneTwoEightK) {
|
||||||
static constexpr int interrupt_duration = 64;
|
constexpr int delays[] = {6, 5, 4, 3, 2, 1, 0, 0};
|
||||||
|
return Timings(228, 311, 4, 128, 14361, delays);
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
if constexpr (timing == Timing::FortyEightK) {
|
||||||
void run_for(HalfCycles duration) {
|
constexpr int delays[] = {6, 5, 4, 3, 2, 1, 0, 0};
|
||||||
static constexpr auto timings = get_timings();
|
return Timings(224, 312, 4, 128, 14335, delays);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static constexpr int sync_line = (timings.interrupt_time / timings.half_cycles_per_line) + 1;
|
// Interrupt should be held for 32 cycles.
|
||||||
|
static constexpr int interrupt_duration = 64;
|
||||||
|
|
||||||
static constexpr int sync_position = (timing == Timing::FortyEightK) ? 164 * 2 : 166 * 2;
|
public:
|
||||||
static constexpr int sync_length = 17 * 2;
|
void run_for(HalfCycles duration) {
|
||||||
static constexpr int burst_position = sync_position + 40;
|
static constexpr auto timings = get_timings();
|
||||||
static constexpr int burst_length = 17;
|
|
||||||
|
|
||||||
int cycles_remaining = duration.as<int>();
|
static constexpr int sync_line = (timings.interrupt_time / timings.half_cycles_per_line) + 1;
|
||||||
while(cycles_remaining) {
|
|
||||||
int line = time_into_frame_ / timings.half_cycles_per_line;
|
|
||||||
int offset = time_into_frame_ % timings.half_cycles_per_line;
|
|
||||||
const int cycles_this_line = std::min(cycles_remaining, timings.half_cycles_per_line - offset);
|
|
||||||
const int end_offset = offset + cycles_this_line;
|
|
||||||
|
|
||||||
if(!offset) {
|
static constexpr int sync_position = (timing == Timing::FortyEightK) ? 164 * 2 : 166 * 2;
|
||||||
is_alternate_line_ ^= true;
|
static constexpr int sync_length = 17 * 2;
|
||||||
|
static constexpr int burst_position = sync_position + 40;
|
||||||
|
static constexpr int burst_length = 17;
|
||||||
|
|
||||||
if(!line) {
|
int cycles_remaining = duration.as<int>();
|
||||||
flash_counter_ = (flash_counter_ + 1) & 31;
|
while(cycles_remaining) {
|
||||||
flash_mask_ = uint8_t(flash_counter_ >> 4);
|
int line = time_into_frame_ / timings.half_cycles_per_line;
|
||||||
}
|
int offset = time_into_frame_ % timings.half_cycles_per_line;
|
||||||
|
const int cycles_this_line = std::min(cycles_remaining, timings.half_cycles_per_line - offset);
|
||||||
|
const int end_offset = offset + cycles_this_line;
|
||||||
|
|
||||||
|
if(!offset) {
|
||||||
|
is_alternate_line_ ^= true;
|
||||||
|
|
||||||
|
if(!line) {
|
||||||
|
flash_counter_ = (flash_counter_ + 1) & 31;
|
||||||
|
flash_mask_ = uint8_t(flash_counter_ >> 4);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(line >= sync_line && line < sync_line + 3) {
|
if(line >= sync_line && line < sync_line + 3) {
|
||||||
// Output sync line.
|
// Output sync line.
|
||||||
crt_.output_sync(cycles_this_line);
|
crt_.output_sync(cycles_this_line);
|
||||||
|
} else {
|
||||||
|
if(line >= 192) {
|
||||||
|
// Output plain border line.
|
||||||
|
if(offset < sync_position) {
|
||||||
|
const int border_duration = std::min(sync_position, end_offset) - offset;
|
||||||
|
crt_.output_level<uint8_t>(border_duration, border_colour_);
|
||||||
|
offset += border_duration;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if(line >= 192) {
|
// Output pixel line.
|
||||||
// Output plain border line.
|
if(offset < 256) {
|
||||||
if(offset < sync_position) {
|
const int pixel_duration = std::min(256, end_offset) - offset;
|
||||||
const int border_duration = std::min(sync_position, end_offset) - offset;
|
|
||||||
crt_.output_level<uint8_t>(border_duration, border_colour_);
|
if(!offset) {
|
||||||
offset += border_duration;
|
pixel_target_ = crt_.begin_data(256);
|
||||||
|
attribute_address_ = ((line >> 3) << 5) + 6144;
|
||||||
|
pixel_address_ = ((line & 0x07) << 8) | ((line & 0x38) << 2) | ((line & 0xc0) << 5);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Output pixel line.
|
|
||||||
if(offset < 256) {
|
|
||||||
const int pixel_duration = std::min(256, end_offset) - offset;
|
|
||||||
|
|
||||||
if(!offset) {
|
if(pixel_target_) {
|
||||||
pixel_target_ = crt_.begin_data(256);
|
const int start_column = offset >> 4;
|
||||||
attribute_address_ = ((line >> 3) << 5) + 6144;
|
const int end_column = (offset + pixel_duration) >> 4;
|
||||||
pixel_address_ = ((line & 0x07) << 8) | ((line & 0x38) << 2) | ((line & 0xc0) << 5);
|
for(int column = start_column; column < end_column; column++) {
|
||||||
}
|
last_fetches_[0] = memory_[pixel_address_];
|
||||||
|
last_fetches_[1] = memory_[attribute_address_];
|
||||||
|
last_fetches_[2] = memory_[pixel_address_+1];
|
||||||
|
last_fetches_[3] = memory_[attribute_address_+1];
|
||||||
|
set_last_contended_area_access(last_fetches_[3]);
|
||||||
|
|
||||||
if(pixel_target_) {
|
pixel_address_ += 2;
|
||||||
const int start_column = offset >> 4;
|
attribute_address_ += 2;
|
||||||
const int end_column = (offset + pixel_duration) >> 4;
|
|
||||||
for(int column = start_column; column < end_column; column++) {
|
|
||||||
last_fetches_[0] = memory_[pixel_address_];
|
|
||||||
last_fetches_[1] = memory_[attribute_address_];
|
|
||||||
last_fetches_[2] = memory_[pixel_address_+1];
|
|
||||||
last_fetches_[3] = memory_[attribute_address_+1];
|
|
||||||
set_last_contended_area_access(last_fetches_[3]);
|
|
||||||
|
|
||||||
pixel_address_ += 2;
|
constexpr uint8_t masks[] = {0, 0xff};
|
||||||
attribute_address_ += 2;
|
|
||||||
|
|
||||||
constexpr uint8_t masks[] = {0, 0xff};
|
|
||||||
|
|
||||||
#define Output(n) \
|
#define Output(n) \
|
||||||
{ \
|
{ \
|
||||||
@@ -198,269 +198,269 @@ template <Timing timing> class Video {
|
|||||||
pixel_target_ += 8; \
|
pixel_target_ += 8; \
|
||||||
}
|
}
|
||||||
|
|
||||||
Output(0);
|
Output(0);
|
||||||
Output(2);
|
Output(2);
|
||||||
|
|
||||||
#undef Output
|
#undef Output
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += pixel_duration;
|
|
||||||
if(offset == 256) {
|
|
||||||
crt_.output_data(256);
|
|
||||||
pixel_target_ = nullptr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(offset >= 256 && offset < sync_position && end_offset > offset) {
|
offset += pixel_duration;
|
||||||
const int border_duration = std::min(sync_position, end_offset) - offset;
|
if(offset == 256) {
|
||||||
crt_.output_level<uint8_t>(border_duration, border_colour_);
|
crt_.output_data(256);
|
||||||
offset += border_duration;
|
pixel_target_ = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output the common tail to border and pixel lines: sync, blank, colour burst, border.
|
if(offset >= 256 && offset < sync_position && end_offset > offset) {
|
||||||
|
const int border_duration = std::min(sync_position, end_offset) - offset;
|
||||||
if(offset >= sync_position && offset < sync_position + sync_length && end_offset > offset) {
|
crt_.output_level<uint8_t>(border_duration, border_colour_);
|
||||||
const int sync_duration = std::min(sync_position + sync_length, end_offset) - offset;
|
offset += border_duration;
|
||||||
crt_.output_sync(sync_duration);
|
|
||||||
offset += sync_duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(offset >= sync_position + sync_length && offset < burst_position && end_offset > offset) {
|
|
||||||
const int blank_duration = std::min(burst_position, end_offset) - offset;
|
|
||||||
crt_.output_blank(blank_duration);
|
|
||||||
offset += blank_duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(offset >= burst_position && offset < burst_position+burst_length && end_offset > offset) {
|
|
||||||
const int burst_duration = std::min(burst_position + burst_length, end_offset) - offset;
|
|
||||||
|
|
||||||
if constexpr (timing >= Timing::OneTwoEightK) {
|
|
||||||
crt_.output_colour_burst(burst_duration, 116, is_alternate_line_);
|
|
||||||
// The colour burst phase above is an empirical guess. I need to research further.
|
|
||||||
} else {
|
|
||||||
crt_.output_default_colour_burst(burst_duration);
|
|
||||||
}
|
|
||||||
offset += burst_duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(offset >= burst_position+burst_length && end_offset > offset) {
|
|
||||||
crt_.output_level<uint8_t>(end_offset - offset, border_colour_);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cycles_remaining -= cycles_this_line;
|
// Output the common tail to border and pixel lines: sync, blank, colour burst, border.
|
||||||
time_into_frame_ = (time_into_frame_ + cycles_this_line) % (timings.half_cycles_per_line * timings.lines_per_frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
if(offset >= sync_position && offset < sync_position + sync_length && end_offset > offset) {
|
||||||
static constexpr int half_cycles_per_line() {
|
const int sync_duration = std::min(sync_position + sync_length, end_offset) - offset;
|
||||||
if constexpr (timing == Timing::FortyEightK) {
|
crt_.output_sync(sync_duration);
|
||||||
// TODO: determine real figure here, if one exists.
|
offset += sync_duration;
|
||||||
// The source I'm looking at now suggests that the theoretical
|
}
|
||||||
// ideal of 224*2 ignores the real-life effects of separate
|
|
||||||
// crystals, so I've nudged this experimentally.
|
|
||||||
return 224*2 - 1;
|
|
||||||
} else {
|
|
||||||
return 227*2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static constexpr HalfCycles frame_duration() {
|
if(offset >= sync_position + sync_length && offset < burst_position && end_offset > offset) {
|
||||||
const auto timings = get_timings();
|
const int blank_duration = std::min(burst_position, end_offset) - offset;
|
||||||
return HalfCycles(timings.half_cycles_per_line * timings.lines_per_frame);
|
crt_.output_blank(blank_duration);
|
||||||
}
|
offset += blank_duration;
|
||||||
|
}
|
||||||
|
|
||||||
HalfCycles time_since_interrupt() {
|
if(offset >= burst_position && offset < burst_position+burst_length && end_offset > offset) {
|
||||||
const auto timings = get_timings();
|
const int burst_duration = std::min(burst_position + burst_length, end_offset) - offset;
|
||||||
if(time_into_frame_ >= timings.interrupt_time) {
|
|
||||||
return HalfCycles(time_into_frame_ - timings.interrupt_time);
|
|
||||||
} else {
|
|
||||||
return HalfCycles(time_into_frame_) + frame_duration() - HalfCycles(timings.interrupt_time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void set_time_since_interrupt(const HalfCycles time) {
|
if constexpr (timing >= Timing::OneTwoEightK) {
|
||||||
// Advance using run_for to ensure that all proper CRT interactions occurred.
|
crt_.output_colour_burst(burst_duration, 116, is_alternate_line_);
|
||||||
const auto timings = get_timings();
|
// The colour burst phase above is an empirical guess. I need to research further.
|
||||||
const auto target = (time + timings.interrupt_time) % frame_duration();
|
} else {
|
||||||
const auto now = HalfCycles(time_into_frame_);
|
crt_.output_default_colour_burst(burst_duration);
|
||||||
|
}
|
||||||
|
offset += burst_duration;
|
||||||
|
}
|
||||||
|
|
||||||
// Maybe this is easy?
|
if(offset >= burst_position+burst_length && end_offset > offset) {
|
||||||
if(target == now) return;
|
crt_.output_level<uint8_t>(end_offset - offset, border_colour_);
|
||||||
|
}
|
||||||
// Is the time within this frame?
|
|
||||||
if(time > now) {
|
|
||||||
run_for(target - time);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then it's necessary to finish this frame and run into the next.
|
cycles_remaining -= cycles_this_line;
|
||||||
run_for(frame_duration() - now + time);
|
time_into_frame_ = (time_into_frame_ + cycles_this_line) % (timings.half_cycles_per_line * timings.lines_per_frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr int half_cycles_per_line() {
|
||||||
|
if constexpr (timing == Timing::FortyEightK) {
|
||||||
|
// TODO: determine real figure here, if one exists.
|
||||||
|
// The source I'm looking at now suggests that the theoretical
|
||||||
|
// ideal of 224*2 ignores the real-life effects of separate
|
||||||
|
// crystals, so I've nudged this experimentally.
|
||||||
|
return 224*2 - 1;
|
||||||
|
} else {
|
||||||
|
return 227*2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr HalfCycles frame_duration() {
|
||||||
|
const auto timings = get_timings();
|
||||||
|
return HalfCycles(timings.half_cycles_per_line * timings.lines_per_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
HalfCycles time_since_interrupt() {
|
||||||
|
const auto timings = get_timings();
|
||||||
|
if(time_into_frame_ >= timings.interrupt_time) {
|
||||||
|
return HalfCycles(time_into_frame_ - timings.interrupt_time);
|
||||||
|
} else {
|
||||||
|
return HalfCycles(time_into_frame_) + frame_duration() - HalfCycles(timings.interrupt_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_time_since_interrupt(const HalfCycles time) {
|
||||||
|
// Advance using run_for to ensure that all proper CRT interactions occurred.
|
||||||
|
const auto timings = get_timings();
|
||||||
|
const auto target = (time + timings.interrupt_time) % frame_duration();
|
||||||
|
const auto now = HalfCycles(time_into_frame_);
|
||||||
|
|
||||||
|
// Maybe this is easy?
|
||||||
|
if(target == now) return;
|
||||||
|
|
||||||
|
// Is the time within this frame?
|
||||||
|
if(time > now) {
|
||||||
|
run_for(target - time);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
// Then it's necessary to finish this frame and run into the next.
|
||||||
Video() :
|
run_for(frame_duration() - now + time);
|
||||||
crt_(half_cycles_per_line(), 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2)
|
}
|
||||||
{
|
|
||||||
// Show only the centre 80% of the TV frame.
|
|
||||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
|
||||||
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f));
|
|
||||||
|
|
||||||
// Get the CRT roughly into phase.
|
public:
|
||||||
//
|
Video() :
|
||||||
// TODO: this is coupled to an assumption about the initial CRT. Fix.
|
crt_(half_cycles_per_line(), 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2)
|
||||||
const auto timings = get_timings();
|
{
|
||||||
crt_.output_blank(timings.lines_per_frame*timings.half_cycles_per_line - timings.interrupt_time);
|
// Show only the centre 80% of the TV frame.
|
||||||
|
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||||
|
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f));
|
||||||
|
|
||||||
|
// Get the CRT roughly into phase.
|
||||||
|
//
|
||||||
|
// TODO: this is coupled to an assumption about the initial CRT. Fix.
|
||||||
|
const auto timings = get_timings();
|
||||||
|
crt_.output_blank(timings.lines_per_frame*timings.half_cycles_per_line - timings.interrupt_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_video_source(const uint8_t *source) {
|
||||||
|
memory_ = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@returns The amount of time until the next change in the interrupt line, that being the only internally-observeable output.
|
||||||
|
*/
|
||||||
|
HalfCycles next_sequence_point() {
|
||||||
|
constexpr auto timings = get_timings();
|
||||||
|
|
||||||
|
// Is the frame still ahead of this interrupt?
|
||||||
|
if(time_into_frame_ < timings.interrupt_time) {
|
||||||
|
return HalfCycles(timings.interrupt_time - time_into_frame_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_video_source(const uint8_t *source) {
|
// If not, is it within this interrupt?
|
||||||
memory_ = source;
|
if(time_into_frame_ < timings.interrupt_time + interrupt_duration) {
|
||||||
|
return HalfCycles(timings.interrupt_time + interrupt_duration - time_into_frame_);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
// If not, it'll be in the next batch.
|
||||||
@returns The amount of time until the next change in the interrupt line, that being the only internally-observeable output.
|
return timings.interrupt_time + timings.half_cycles_per_line * timings.lines_per_frame - time_into_frame_;
|
||||||
*/
|
}
|
||||||
HalfCycles next_sequence_point() {
|
|
||||||
constexpr auto timings = get_timings();
|
|
||||||
|
|
||||||
// Is the frame still ahead of this interrupt?
|
/*!
|
||||||
if(time_into_frame_ < timings.interrupt_time) {
|
@returns The current state of the interrupt output.
|
||||||
return HalfCycles(timings.interrupt_time - time_into_frame_);
|
*/
|
||||||
}
|
bool get_interrupt_line() const {
|
||||||
|
constexpr auto timings = get_timings();
|
||||||
|
return time_into_frame_ >= timings.interrupt_time && time_into_frame_ < timings.interrupt_time + interrupt_duration;
|
||||||
|
}
|
||||||
|
|
||||||
// If not, is it within this interrupt?
|
/*!
|
||||||
if(time_into_frame_ < timings.interrupt_time + interrupt_duration) {
|
@returns How many cycles the [ULA/gate array] would delay the CPU for if it were to recognise that contention
|
||||||
return HalfCycles(timings.interrupt_time + interrupt_duration - time_into_frame_);
|
needs to be applied in @c offset half-cycles from now.
|
||||||
}
|
*/
|
||||||
|
HalfCycles access_delay(HalfCycles offset) const {
|
||||||
|
constexpr auto timings = get_timings();
|
||||||
|
const int delay_time = (time_into_frame_ + offset.as<int>() + timings.contention_leadin) % (timings.half_cycles_per_line * timings.lines_per_frame);
|
||||||
|
assert(!(delay_time&1));
|
||||||
|
|
||||||
// If not, it'll be in the next batch.
|
// Check for a time within the no-contention window.
|
||||||
return timings.interrupt_time + timings.half_cycles_per_line * timings.lines_per_frame - time_into_frame_;
|
if(delay_time >= (191*timings.half_cycles_per_line + timings.contention_duration)) {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
const int time_into_line = delay_time % timings.half_cycles_per_line;
|
||||||
@returns The current state of the interrupt output.
|
if(time_into_line >= timings.contention_duration) {
|
||||||
*/
|
return 0;
|
||||||
bool get_interrupt_line() const {
|
|
||||||
constexpr auto timings = get_timings();
|
|
||||||
return time_into_frame_ >= timings.interrupt_time && time_into_frame_ < timings.interrupt_time + interrupt_duration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
return HalfCycles(timings.delays[(time_into_line >> 1) & 7]);
|
||||||
@returns How many cycles the [ULA/gate array] would delay the CPU for if it were to recognise that contention
|
}
|
||||||
needs to be applied in @c offset half-cycles from now.
|
|
||||||
*/
|
|
||||||
HalfCycles access_delay(HalfCycles offset) const {
|
|
||||||
constexpr auto timings = get_timings();
|
|
||||||
const int delay_time = (time_into_frame_ + offset.as<int>() + timings.contention_leadin) % (timings.half_cycles_per_line * timings.lines_per_frame);
|
|
||||||
assert(!(delay_time&1));
|
|
||||||
|
|
||||||
// Check for a time within the no-contention window.
|
/*!
|
||||||
if(delay_time >= (191*timings.half_cycles_per_line + timings.contention_duration)) {
|
@returns Whatever the ULA or gate array would expose via the floating bus, this cycle.
|
||||||
return 0;
|
*/
|
||||||
}
|
uint8_t get_floating_value() const {
|
||||||
|
constexpr auto timings = get_timings();
|
||||||
|
const uint8_t out_of_bounds = (timing == Timing::Plus3) ? last_contended_access_ : 0xff;
|
||||||
|
|
||||||
const int time_into_line = delay_time % timings.half_cycles_per_line;
|
const int line = time_into_frame_ / timings.half_cycles_per_line;
|
||||||
if(time_into_line >= timings.contention_duration) {
|
if(line >= 192) {
|
||||||
return 0;
|
return out_of_bounds;
|
||||||
}
|
|
||||||
|
|
||||||
return HalfCycles(timings.delays[(time_into_line >> 1) & 7]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
const int time_into_line = time_into_frame_ % timings.half_cycles_per_line;
|
||||||
@returns Whatever the ULA or gate array would expose via the floating bus, this cycle.
|
if(time_into_line >= 256 || (time_into_line&8)) {
|
||||||
*/
|
return out_of_bounds;
|
||||||
uint8_t get_floating_value() const {
|
|
||||||
constexpr auto timings = get_timings();
|
|
||||||
const uint8_t out_of_bounds = (timing == Timing::Plus3) ? last_contended_access_ : 0xff;
|
|
||||||
|
|
||||||
const int line = time_into_frame_ / timings.half_cycles_per_line;
|
|
||||||
if(line >= 192) {
|
|
||||||
return out_of_bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int time_into_line = time_into_frame_ % timings.half_cycles_per_line;
|
|
||||||
if(time_into_line >= 256 || (time_into_line&8)) {
|
|
||||||
return out_of_bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The +2a and +3 always return the low bit as set.
|
|
||||||
const uint8_t value = last_fetches_[(time_into_line >> 1) & 3];
|
|
||||||
if constexpr (timing == Timing::Plus3) {
|
|
||||||
return value | 1;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
// The +2a and +3 always return the low bit as set.
|
||||||
Relevant to the +2a and +3 only, sets the most recent value read from or
|
const uint8_t value = last_fetches_[(time_into_line >> 1) & 3];
|
||||||
written to contended memory. This is what will be returned if the floating
|
if constexpr (timing == Timing::Plus3) {
|
||||||
bus is accessed when the gate array isn't currently reading.
|
return value | 1;
|
||||||
*/
|
|
||||||
void set_last_contended_area_access([[maybe_unused]] uint8_t value) {
|
|
||||||
if constexpr (timing == Timing::Plus3) {
|
|
||||||
last_contended_access_ = value | 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
Sets the current border colour.
|
Relevant to the +2a and +3 only, sets the most recent value read from or
|
||||||
*/
|
written to contended memory. This is what will be returned if the floating
|
||||||
void set_border_colour(uint8_t colour) {
|
bus is accessed when the gate array isn't currently reading.
|
||||||
border_byte_ = colour;
|
*/
|
||||||
border_colour_ = palette[colour];
|
void set_last_contended_area_access([[maybe_unused]] uint8_t value) {
|
||||||
|
if constexpr (timing == Timing::Plus3) {
|
||||||
|
last_contended_access_ = value | 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the scan target.
|
/*!
|
||||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
Sets the current border colour.
|
||||||
crt_.set_scan_target(scan_target);
|
*/
|
||||||
}
|
void set_border_colour(uint8_t colour) {
|
||||||
|
border_byte_ = colour;
|
||||||
|
border_colour_ = palette[colour];
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets the current scan status.
|
/// Sets the scan target.
|
||||||
Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
void set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||||
return crt_.get_scaled_scan_status();
|
crt_.set_scan_target(scan_target);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*! Sets the type of display the CRT will request. */
|
/// Gets the current scan status.
|
||||||
void set_display_type(Outputs::Display::DisplayType type) {
|
Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
||||||
crt_.set_display_type(type);
|
return crt_.get_scaled_scan_status();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*! Gets the display type. */
|
/*! Sets the type of display the CRT will request. */
|
||||||
Outputs::Display::DisplayType get_display_type() const {
|
void set_display_type(Outputs::Display::DisplayType type) {
|
||||||
return crt_.get_display_type();
|
crt_.set_display_type(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
/*! Gets the display type. */
|
||||||
int time_into_frame_ = 0;
|
Outputs::Display::DisplayType get_display_type() const {
|
||||||
Outputs::CRT::CRT crt_;
|
return crt_.get_display_type();
|
||||||
const uint8_t *memory_ = nullptr;
|
}
|
||||||
uint8_t border_colour_ = 0;
|
|
||||||
uint8_t border_byte_ = 0;
|
|
||||||
|
|
||||||
uint8_t *pixel_target_ = nullptr;
|
private:
|
||||||
int attribute_address_ = 0;
|
int time_into_frame_ = 0;
|
||||||
int pixel_address_ = 0;
|
Outputs::CRT::CRT crt_;
|
||||||
|
const uint8_t *memory_ = nullptr;
|
||||||
|
uint8_t border_colour_ = 0;
|
||||||
|
uint8_t border_byte_ = 0;
|
||||||
|
|
||||||
uint8_t flash_mask_ = 0;
|
uint8_t *pixel_target_ = nullptr;
|
||||||
int flash_counter_ = 0;
|
int attribute_address_ = 0;
|
||||||
bool is_alternate_line_ = false;
|
int pixel_address_ = 0;
|
||||||
|
|
||||||
uint8_t last_fetches_[4] = {0xff, 0xff, 0xff, 0xff};
|
uint8_t flash_mask_ = 0;
|
||||||
uint8_t last_contended_access_ = 0xff;
|
int flash_counter_ = 0;
|
||||||
|
bool is_alternate_line_ = false;
|
||||||
|
|
||||||
friend struct State;
|
uint8_t last_fetches_[4] = {0xff, 0xff, 0xff, 0xff};
|
||||||
|
uint8_t last_contended_access_ = 0xff;
|
||||||
|
|
||||||
|
friend struct State;
|
||||||
|
|
||||||
#define RGB(r, g, b) (r << 4) | (g << 2) | b
|
#define RGB(r, g, b) (r << 4) | (g << 2) | b
|
||||||
static constexpr uint8_t palette[] = {
|
static constexpr uint8_t palette[] = {
|
||||||
RGB(0, 0, 0), RGB(0, 0, 2), RGB(2, 0, 0), RGB(2, 0, 2),
|
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, 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, 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),
|
RGB(0, 3, 0), RGB(0, 3, 3), RGB(3, 3, 0), RGB(3, 3, 3),
|
||||||
};
|
};
|
||||||
#undef RGB
|
#undef RGB
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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,
|
AcornADFS,
|
||||||
PRESAdvancedPlus6,
|
PRESAdvancedPlus6,
|
||||||
Acorn1770DFS,
|
Acorn1770DFS,
|
||||||
|
AcornIDEADFS103,
|
||||||
|
|
||||||
// Acorn Archimedes.
|
// Acorn Archimedes.
|
||||||
AcornArthur030,
|
AcornArthur030,
|
||||||
@@ -223,8 +224,20 @@ struct Description {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
template <typename FileNameT, typename CRC32T> Description(
|
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)
|
const Name name,
|
||||||
) : name{name}, machine_name{machine_name}, descriptive_name{descriptive_name}, file_names{file_names}, size{size}, crc32s{crc32s} {
|
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
|
// 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
|
// 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.
|
// would be to provide a partial specialisation that never put anything into the set.
|
||||||
@@ -232,6 +245,8 @@ private:
|
|||||||
this->crc32s.clear();
|
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
|
/// @returns a vector of all possible instances of ROM::Description — i.e. descriptions of every ROM
|
||||||
|
@@ -16,7 +16,7 @@ namespace Numeric {
|
|||||||
/// keeping the least-significant bit in its original position.
|
/// keeping the least-significant bit in its original position.
|
||||||
///
|
///
|
||||||
/// i.e. if @c input is abcdefgh then the result is 0a0b0c0d0e0f0g0h
|
/// 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
|
uint16_t result = uint16_t(input); // 0000 0000 abcd efgh
|
||||||
result = (result | (result << 4)) & 0x0f0f; // 0000 abcd 0000 efgh
|
result = (result | (result << 4)) & 0x0f0f; // 0000 abcd 0000 efgh
|
||||||
result = (result | (result << 2)) & 0x3333; // 00ab 00cd 00ef 00gh
|
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
|
/// 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
|
/// @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.
|
/// i.e. every other bit is retained, keeping the least-significant bit in place.
|
||||||
constexpr uint8_t unspread_bits(uint16_t input) {
|
constexpr uint8_t unspread_bits(const uint16_t input) {
|
||||||
input &= 0x5555; // 0a0b 0c0d 0e0f 0g0h
|
uint16_t result = input;
|
||||||
input = (input | (input >> 1)) & 0x3333; // 00ab 00cd 00ef 00gh
|
result &= 0x5555; // 0a0b 0c0d 0e0f 0g0h
|
||||||
input = (input | (input >> 2)) & 0x0f0f; // 0000 abcd 0000 efgh
|
result = (result | (result >> 1)) & 0x3333; // 00ab 00cd 00ef 00gh
|
||||||
return uint8_t(input | (input >> 4)); // 0000 0000 abcd efgh
|
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.
|
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,
|
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 {
|
template <int... Sizes> class NumericCoder {
|
||||||
public:
|
public:
|
||||||
/// Modifies @c target to hold @c value at @c index.
|
/// 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");
|
static_assert(index < sizeof...(Sizes), "Index must be within range");
|
||||||
NumericEncoder<Sizes...>::template encode<index>(target, value);
|
NumericEncoder<Sizes...>::template encode<index>(target, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @returns The value from @c source at @c index.
|
/// @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");
|
static_assert(index < sizeof...(Sizes), "Index must be within range");
|
||||||
return NumericDecoder<Sizes...>::template decode<index>(source);
|
return NumericDecoder<Sizes...>::template decode<index>(source);
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ private:
|
|||||||
|
|
||||||
template <int size, int... Tail>
|
template <int size, int... Tail>
|
||||||
struct NumericEncoder {
|
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) {
|
if constexpr (i == index) {
|
||||||
const int suffix = target % divider;
|
const int suffix = target % divider;
|
||||||
target /= divider;
|
target /= divider;
|
||||||
@@ -61,7 +61,7 @@ private:
|
|||||||
|
|
||||||
template <int size, int... Tail>
|
template <int size, int... Tail>
|
||||||
struct NumericDecoder {
|
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) {
|
if constexpr (i == index) {
|
||||||
return (source / divider) % size;
|
return (source / divider) % size;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -62,7 +62,7 @@
|
|||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Release"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
enableASanStackUseAfterReturn = "YES"
|
enableASanStackUseAfterReturn = "YES"
|
||||||
|
@@ -76,7 +76,7 @@ enum class EnabledLevel {
|
|||||||
|
|
||||||
constexpr EnabledLevel enabled_level(const Source source) {
|
constexpr EnabledLevel enabled_level(const Source source) {
|
||||||
#ifdef NDEBUG
|
#ifdef NDEBUG
|
||||||
return false;
|
return EnabledLevel::None;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Allow for compile-time source-level enabling and disabling of different sources.
|
// 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(glShaderSource, shader, 1, &c_str, NULL);
|
||||||
test_gl(glCompileShader, shader);
|
test_gl(glCompileShader, shader);
|
||||||
|
|
||||||
if constexpr (logger.enabled) {
|
if constexpr (logger.ErrorsEnabled) {
|
||||||
GLint isCompiled = 0;
|
GLint isCompiled = 0;
|
||||||
test_gl(glGetShaderiv, shader, GL_COMPILE_STATUS, &isCompiled);
|
test_gl(glGetShaderiv, shader, GL_COMPILE_STATUS, &isCompiled);
|
||||||
if(isCompiled == GL_FALSE) {
|
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) {
|
for(const auto &binding : attribute_bindings) {
|
||||||
test_gl(glBindAttribLocation, shader_program_, binding.index, binding.name.c_str());
|
test_gl(glBindAttribLocation, shader_program_, binding.index, binding.name.c_str());
|
||||||
|
|
||||||
if constexpr (logger.enabled) {
|
if constexpr (logger.ErrorsEnabled) {
|
||||||
const auto error = glGetError();
|
const auto error = glGetError();
|
||||||
switch(error) {
|
switch(error) {
|
||||||
case 0: break;
|
case 0: break;
|
||||||
@@ -88,7 +88,7 @@ void Shader::init(const std::string &vertex_shader, const std::string &fragment_
|
|||||||
|
|
||||||
test_gl(glLinkProgram, shader_program_);
|
test_gl(glLinkProgram, shader_program_);
|
||||||
|
|
||||||
if constexpr (logger.enabled) {
|
if constexpr (logger.ErrorsEnabled) {
|
||||||
GLint logLength;
|
GLint logLength;
|
||||||
test_gl(glGetProgramiv, shader_program_, GL_INFO_LOG_LENGTH, &logLength);
|
test_gl(glGetProgramiv, shader_program_, GL_INFO_LOG_LENGTH, &logLength);
|
||||||
if(logLength > 0) {
|
if(logLength > 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_1.rom — used only if the user opens an ADFS disk image
|
||||||
ADFS-E00_2.rom
|
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.
|
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:
|
Possibly to be desired in the future:
|
||||||
* os300.rom
|
* os300.rom
|
||||||
|
Reference in New Issue
Block a user