1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-09-12 02:24:31 +00:00

Compare commits

...

13 Commits

Author SHA1 Message Date
Thomas Harte
cad42beef4 Roll in some random style improvements. 2025-09-08 20:38:50 -04:00
Thomas Harte
96fd0b7892 Merge pull request #1553 from TomHarte/IndentationSomeMore
Further reduce indentation.
2025-09-05 23:20:19 -04:00
Thomas Harte
6f1db15d7c Further reduce indentation. 2025-09-05 23:07:45 -04:00
Thomas Harte
1854296ee8 Merge pull request #1552 from TomHarte/ElectronIDE
Reduce code duplication within the ROM catalogue.
2025-09-05 22:45:55 -04:00
Thomas Harte
515cc5f326 Correct spelling. 2025-09-05 22:09:38 -04:00
Thomas Harte
091be7eafe Remove unused header. 2025-09-05 22:03:45 -04:00
Thomas Harte
27a19ea417 Eliminate line-length violations. 2025-09-05 22:03:19 -04:00
Thomas Harte
9a5e9af67c Standardise layout. 2025-09-05 22:00:42 -04:00
Thomas Harte
3a493f2428 Merge pull request #1551 from TomHarte/LogLevels
Allow logging of errors but not info.
2025-09-05 21:04:56 -04:00
Thomas Harte
ca6e34f4b4 Fix dangling OpenGL accesses. 2025-09-05 19:30:33 -04:00
Thomas Harte
e1e68312c4 Transcribe remaining catalogue entries. 2025-09-05 17:23:38 -04:00
Thomas Harte
c7ff2cece4 Head in search of a more-compact form. 2025-09-05 16:55:00 -04:00
Thomas Harte
8e6f4fa36f Fix NDEBUG route. 2025-09-05 14:34:08 -04:00
30 changed files with 5022 additions and 4187 deletions

View File

@@ -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) {}

View File

@@ -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 {

View File

@@ -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_;
}; };
} }

View File

@@ -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_;
}; };
} }

View File

@@ -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_;
}; };

View File

@@ -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;
}; };
} }

View File

@@ -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;
}; };
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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_;
}; };
} }

View File

@@ -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 0x400x7f.
update_audio();
sn76489_.write(*cycle.value);
break;
case 0x80: case 0x81: // i.e. ports 0x800xbf.
vdp_->write(address, *cycle.value);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
break;
case 0xc1: case 0xc0: // i.e. ports 0xc00xff.
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 0x400x7f.
update_audio();
sn76489_.write(*cycle.value);
break;
case 0x80: case 0x81: // i.e. ports 0x800xbf.
vdp_->write(address, *cycle.value);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
break;
case 0xc1: case 0xc0: // i.e. ports 0xc00xff.
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;
}; };
} }

View File

@@ -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);
}
}; };
} }

View File

@@ -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_;
}; };
} }

View File

@@ -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);
}
}; };
} }

View File

@@ -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

View File

@@ -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_;
}; };
} }

View File

@@ -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_;
}; };
} }

View File

@@ -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);
}; };
} }

View File

@@ -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)));
}
}; };
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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
} }
} }

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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