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

Compare commits

...

42 Commits

Author SHA1 Message Date
Thomas Harte
035713b4d3 Remove logging. 2025-09-10 23:00:42 -04:00
Thomas Harte
54b7dc56b5 Resolve risk of acknowledged interrupt refiring. 2025-09-10 22:59:57 -04:00
Thomas Harte
7fd39f44d0 Add some logging, take a stab at returning requests. 2025-09-10 21:46:58 -04:00
Thomas Harte
691292501a Promote constexprs to static. 2025-09-10 21:46:44 -04:00
Thomas Harte
a58158ae08 Add PIT and PIC. 2025-09-10 21:45:51 -04:00
Thomas Harte
ef09b971fa Watch software interrupt flags.
Now tracking: issue seems to be reaching TEST4.ASM:D11 with an interrupt that it believes to be software-originating.
2025-09-10 15:47:19 -04:00
Thomas Harte
e07dee380d Experiment with further delays. 2025-09-10 14:18:30 -04:00
Thomas Harte
125bc5baa6 Install communication delay. 2025-09-10 13:48:51 -04:00
Thomas Harte
995444b11b Add TODO on what seems to be the current issue. 2025-09-10 11:33:38 -04:00
Thomas Harte
8f2384dbfc Fix log entry interleaving. 2025-09-10 09:52:55 -04:00
Thomas Harte
0cdd1c23ce Guess at another ID.
Cf. https://stanislavs.org/helppc/keyboard_commands.html
2025-09-09 23:40:55 -04:00
Thomas Harte
4765a39759 New guess: writing to the keyboard implicitly enables communications. 2025-09-09 23:38:21 -04:00
Thomas Harte
7f4047772c Continue naming things. 2025-09-09 15:36:02 -04:00
Thomas Harte
45c4ca6bec Attempt further to elide storage. 2025-09-09 13:58:37 -04:00
Thomas Harte
4a573a5aae Clarify one magic constant. 2025-09-09 13:44:31 -04:00
Thomas Harte
5125ff6a8c Combine enables, silence port 61 for now. 2025-09-09 11:16:42 -04:00
Thomas Harte
482d3301ce Avoid faulty sector access. 2025-09-08 23:14:50 -04:00
Thomas Harte
cdeec8ac47 Take various more failed swings at which bits do what. 2025-09-08 22:54:10 -04:00
Thomas Harte
3cef12b53b Reintroduce proper ordering of log comments. 2025-09-08 22:27:40 -04:00
Thomas Harte
dd098a16a8 Log more. 2025-09-08 21:54:56 -04:00
Thomas Harte
61a175e84a Merge branch 'master' into Keyboard 2025-09-08 21:43:25 -04:00
Thomas Harte
a5bcd38fe8 Slightly reformat. 2025-09-08 21:43:18 -04:00
Thomas Harte
cad42beef4 Roll in some random style improvements. 2025-09-08 20:38:50 -04:00
Thomas Harte
5a57958639 Reduce log repetition. 2025-09-08 17:22:53 -04:00
Thomas Harte
260336c1e5 Adopt phase as communicative of whether more bytes are expected. 2025-09-08 17:13:27 -04:00
Thomas Harte
889cb9c78f Attempt a dual-queue solution to enabling/disabling keyboard. 2025-09-08 14:40:08 -04:00
Thomas Harte
b90e8f5af3 Further tweak reporting. 2025-09-06 23:16:10 -04:00
Thomas Harte
12361d2854 Adopt proper error/info distinction. 2025-09-06 23:13:33 -04:00
Thomas Harte
d307ddfa8e Merge branch 'master' into Keyboard 2025-09-05 23:21:41 -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
Thomas Harte
e0917dc734 Switch focus back to keyboard. 2025-09-04 22:21:05 -04:00
36 changed files with 5298 additions and 4302 deletions

View File

@@ -96,7 +96,7 @@ public:
target = (target & 0xff00) | value; target = (target & 0xff00) | value;
}; };
const auto load_high = [value](uint16_t &target) { const auto load_high = [value](uint16_t &target) {
constexpr uint8_t mask = RefreshMask >> 8; static constexpr uint8_t mask = RefreshMask >> 8;
target = uint16_t((target & 0x00ff) | ((value & mask) << 8)); target = uint16_t((target & 0x00ff) | ((value & mask) << 8));
}; };

View File

@@ -18,7 +18,7 @@ struct Vector {
int v[2]{}; int v[2]{};
template <int offset, bool high> void set(const uint8_t value) { template <int offset, bool high> void set(const uint8_t value) {
constexpr uint8_t mask = high ? (offset ? 0x3 : 0x1) : 0xff; static constexpr uint8_t mask = high ? (offset ? 0x3 : 0x1) : 0xff;
static constexpr int shift = high ? 8 : 0; static constexpr int shift = high ? 8 : 0;
v[offset] = (v[offset] & ~(mask << shift)) | ((value & mask) << shift); v[offset] = (v[offset] & ~(mask << shift)) | ((value & mask) << shift);
} }

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

@@ -217,14 +217,21 @@ public:
if(!sector) { if(!sector) {
status_.set(Intel::i8272::Status1::EndOfCylinder); status_.set(Intel::i8272::Status1::EndOfCylinder);
status_.set(Intel::i8272::Status0::AbnormalTermination); status_.set(Intel::i8272::Status0::AbnormalTermination);
}
results_.serialise( results_.serialise(
status_, status_,
sector->address.track, 0,
sector->address.side, 0,
sector->address.sector, 0,
sector->size); 0);
} else {
results_.serialise(
status_,
sector->address.track,
sector->address.side,
sector->address.sector,
sector->size);
}
drive.status = decoder_.drive_head(); drive.status = decoder_.drive_head();
drive.raised_interrupt = true; drive.raised_interrupt = true;

View File

@@ -120,7 +120,7 @@ private:
uint8_t controller_data_; uint8_t controller_data_;
uint8_t controller_status_; uint8_t controller_status_;
[[no_unique_address]] Log::Logger<Log::Source::IDE> log_; mutable Log::Logger<Log::Source::IDE> log_;
}; };
} }

View File

@@ -93,8 +93,8 @@ public:
return key; return key;
} }
auto &keyboard() { void post_keyboard(const uint8_t value) {
return *this; post(value);
} }
void post(const uint8_t value) { void post(const uint8_t value) {
@@ -126,18 +126,66 @@ private:
*/ */
template <Analyser::Static::PCCompatible::Model model> template <Analyser::Static::PCCompatible::Model model>
class KeyboardController<model, typename std::enable_if_t<is_at(model)>> { class KeyboardController<model, typename std::enable_if_t<is_at(model)>> {
private:
template <int delay>
struct ByteQueue {
public:
void append(const std::initializer_list<uint8_t> values) {
if(delay && queue_.empty()) {
restart_delay();
}
// Insert in reverse order, at the start of the vector. All outgoing values
// are popped from the back. So inserts are expensive, reads are cheap.
queue_.insert(queue_.begin(), std::rbegin(values), std::rend(values));
}
bool empty() const {
if(delay && delay_count_) {
return true;
}
return queue_.empty();
}
void restart_delay() {
delay_count_ = delay;
}
uint8_t next() {
const auto next = queue_.back();
queue_.pop_back();
restart_delay();
return next;
}
bool run_for(const int ticks) {
if(!delay_count_) return false;
delay_count_ = std::max(delay_count_ - ticks, 0);
return !delay_count_ && !queue_.empty();
}
private:
std::vector<uint8_t> queue_;
int delay_count_ = 0;
};
public: public:
KeyboardController( KeyboardController(
PICs<model> &pics, PICs<model> &pics,
Speaker &speaker, Speaker &speaker,
const Analyser::Static::PCCompatible::Target::VideoAdaptor adaptor const Analyser::Static::PCCompatible::Target::VideoAdaptor adaptor
) : pics_(pics), speaker_(speaker), keyboard_(*this) { ) : pics_(pics), speaker_(speaker) {
if(adaptor == Analyser::Static::PCCompatible::Target::VideoAdaptor::MDA) { if(adaptor == Analyser::Static::PCCompatible::Target::VideoAdaptor::MDA) {
switches_ |= 0x40; switches_ |= 0x40;
} }
} }
void run_for(const Cycles cycles) { void run_for(const Cycles cycles) {
const bool output_advanced = output_.run_for(cycles.as<int>());
const bool keyboard_advanced = keyboard_.run_for(cycles.as<int>());
if(output_advanced || keyboard_advanced) {
log_.info().append("Advancing output");
check_irqs();
}
instruction_count_ += cycles.as<int>(); instruction_count_ += cycles.as<int>();
if(!perform_delay_) { if(!perform_delay_) {
@@ -151,8 +199,9 @@ public:
} }
} }
auto &keyboard() { void post_keyboard(const uint8_t value) {
return keyboard_; keyboard_.post(value);
check_irqs();
} }
void write(const uint16_t port, const uint8_t value) { void write(const uint16_t port, const uint8_t value) {
@@ -162,7 +211,7 @@ public:
break; break;
case 0x0060: case 0x0060:
log_.error().append("Keyboard parameter set to %02x", value); log_.info().append("Keyboard parameter set to %02x", value);
phase_ = Phase::Data; phase_ = Phase::Data;
input_ = value; input_ = value;
has_input_ = true; has_input_ = true;
@@ -170,6 +219,7 @@ public:
break; break;
case 0x0061: case 0x0061:
// log_.info().append("Port 61: %02x", value);
// TODO: // TODO:
// b7: 1 = reset IRQ 0 // b7: 1 = reset IRQ 0
// b3: enable channel check // b3: enable channel check
@@ -178,12 +228,11 @@ public:
break; break;
case 0x0064: case 0x0064:
phase_ = Phase::Command; log_.info().append("Command byte: %02x", value);
command_ = Command(value); command_ = Command(value);
has_command_ = true; has_command_ = true;
has_input_ = false; has_input_ = false;
perform_delay_ = performance_delay(command_); perform_delay_ = performance_delay(command_);
perform_command(); perform_command();
break; break;
} }
@@ -197,14 +246,12 @@ public:
case 0x0060: { case 0x0060: {
if(has_output()) { if(has_output()) {
advance_output_queue_pointer(output_read_); last_output_ = next_output();
keyboard_.output().restart_delay();
if(!has_output()) { check_irqs();
pics_.pic[0].template apply_edge<1>(false);
}
} }
log_.error().append("Read from keyboard controller of %02x", output_queue_[output_read_]); log_.info().append("Read from keyboard controller of %02x", last_output_);
return output_queue_[output_read_]; return last_output_;
} }
case 0x0061: case 0x0061:
@@ -220,18 +267,18 @@ public:
// b7 = 1 => parity error on transmission; // b7 = 1 => parity error on transmission;
// b6 = 1 => receive timeout; // b6 = 1 => receive timeout;
// b5 = 1 => transmit timeout; // b5 = 1 => transmit timeout;
// b4 = 1 => keyboard active; // b4 = 1 => keyboard enabled via physical key;
// b3 = 1 = data at 0060 is command, 0 = data; // b3 = 1 = data at 0060 is command, 0 = data;
// b2 = 1 = selftest OK; 0 = just powered up or reset; // b2 = 1 = selftest OK; 0 = just powered up or reset;
// b1 = 1 => 'input' buffer full (i.e. don't write 0x60 or 0x64 now — this is input to the controller); // b1 = 1 => 'input' buffer full (i.e. don't write 0x60 or 0x64 now — this is input to the controller);
// b0 = 1 => 'output' data is full (i.e. reading from 0x60 now makes sense — output is to PC). // b0 = 1 => 'output' data is full (i.e. reading from 0x60 now makes sense — output is to PC).
const uint8_t status = const uint8_t status =
(enabled_ ? 0x10 : 0x00) | 0x10 |
(phase_ == Phase::Command ? 0x08 : 0x00) | (phase_ == Phase::Command ? 0x08 : 0x00) |
(is_tested_ ? 0x04 : 0x00) | (is_tested_ ? 0x04 : 0x00) |
(has_input_ ? 0x02 : 0x00) | (has_input_ ? 0x02 : 0x00) |
(has_output() ? 0x01 : 0x00); (has_output() ? 0x01 : 0x00);
// log_.error().append("Reading status: %02x", status); log_.info().append("Reading status: %02x", status);
return status; return status;
} }
} }
@@ -262,6 +309,11 @@ private:
ResetBlockBegin = 0xf0, ResetBlockBegin = 0xf0,
}; };
enum Control: uint8_t {
AllowKeyboardInterrupts = 0x01,
InhibitKeyboard = 0x10,
};
static constexpr bool requires_parameter(const Command command) { static constexpr bool requires_parameter(const Command command) {
return return
(command >= 0x60 && command < 0x80) || (command >= 0x60 && command < 0x80) ||
@@ -282,12 +334,13 @@ private:
void transmit(const uint8_t value) { void transmit(const uint8_t value) {
log_.info().append("Enquing %02x", value); log_.info().append("Enquing %02x", value);
advance_output_queue_pointer(output_write_); output_.append({value});
output_queue_[output_write_] = value; check_irqs();
pics_.pic[0].template apply_edge<1>(true); // TODO: verify.
} }
void perform_command() { void perform_command() {
phase_ = Phase::Data;
// Don't do anything until perform_delay_ is 0 and a command and/or other input is ready. // Don't do anything until perform_delay_ is 0 and a command and/or other input is ready.
if(perform_delay_ || (!has_input_ && !has_command_)) { if(perform_delay_ || (!has_input_ && !has_command_)) {
return; return;
@@ -295,43 +348,42 @@ private:
// No command => input only, which is a direct-to-device communication. // No command => input only, which is a direct-to-device communication.
if(!has_command_) { if(!has_command_) {
if(!enabled_) {
log_.info().append("Storing device command for later: %02x", input_);
keyboard_command_ = input_;
has_input_ = false;
return;
}
log_.info().append("Device command: %02x", input_); log_.info().append("Device command: %02x", input_);
control_ &= ~Control::InhibitKeyboard;
keyboard_.perform(input_); keyboard_.perform(input_);
// TODO: mouse? // TODO: mouse?
has_input_ = false; has_input_ = false;
check_irqs();
return; return;
} }
// There is a command, but stop anyway if it requires a parameter and doesn't yet have one. // There is a command, but stop anyway if it requires a parameter and doesn't yet have one.
if(requires_parameter(command_) && !has_input_) { if(requires_parameter(command_) && !has_input_) {
phase_ = Phase::Command;
return; return;
} }
log_.info().append("Controller command: %02x", command_).append_if(has_input_, " / %02x", input_); log_.info().append("Performing: %02x", command_).append_if(has_input_, " / %02x", input_);
// Consume command and parameter, and execute. // Consume command and parameter, and execute.
has_command_ = false; has_command_ = false;
if(requires_parameter(command_)) has_input_ = false; if(requires_parameter(command_)) has_input_ = false;
if(command_ >= Command::ResetBlockBegin) { if(command_ >= Command::ResetBlockBegin) {
log_.error().append("Should reset: %x", command_ & 0x0f); log_.info().append("Should reset: %x", command_ & 0x0f);
if(!(command_ & 1)) { if(!(command_ & 1)) {
cpu_control_->reset(); cpu_control_->reset();
} }
} else switch(command_) { } else switch(command_) {
default: default:
log_.info().append("Keyboard command unimplemented", command_); log_.info().append("Unimplemented keyboard controller command: %02x", command_);
break; break;
case Command::WriteCommandByte: case Command::WriteCommandByte:
is_tested_ = input_ & 0x4; control_ = input_;
check_irqs();
// TODO: // TODO:
// b0: 1 = enable first PS/2 port interrupt; // b0: 1 = enable first PS/2 port interrupt;
// b1: 1 = enable second port interrupt; // b1: 1 = enable second port interrupt;
@@ -349,22 +401,20 @@ private:
break; break;
case Command::InterfaceTest: case Command::InterfaceTest:
transmit(0); // i.e. no issues uncovered. transmit(0); // i.e. no issues uncovered.
// should_log = false;
break; break;
case Command::ReadTestInputs: case Command::ReadTestInputs:
transmit(enabled_ ? 0x01 : 0x00); // b0 is the keyboard clock; ensure it's inhibited when asked but otherwise don't attempt realism.
transmit((control_ & Control::InhibitKeyboard) ? 0x00 : 0x01);
break; break;
case Command::DisableKeyboard: enabled_ = false; break; case Command::DisableKeyboard:
control_ |= Control::InhibitKeyboard;
check_irqs();
break;
case Command::EnableKeyboard: case Command::EnableKeyboard:
enabled_ = true; control_ &= ~Control::InhibitKeyboard;
check_irqs();
// If a keybaord command was enqueued, post it now.
if(keyboard_command_.has_value()) {
input_ = *keyboard_command_;
keyboard_command_ = std::nullopt;
has_input_ = true;
perform_command();
}
break; break;
case Command::SetOutputByte: case Command::SetOutputByte:
@@ -383,6 +433,7 @@ private:
PICs<model> &pics_; PICs<model> &pics_;
Speaker &speaker_; Speaker &speaker_;
CPUControl<model> *cpu_control_ = nullptr; CPUControl<model> *cpu_control_ = nullptr;
uint8_t control_ = 0;
// Strongly coupled to specific code in the 5170 BIOS, this provides a grossly-inaccurate // Strongly coupled to specific code in the 5170 BIOS, this provides a grossly-inaccurate
// linkage between execution speed (-ish) and DRAM refresh. An unambguous nonsense. // linkage between execution speed (-ish) and DRAM refresh. An unambguous nonsense.
@@ -390,21 +441,13 @@ private:
uint8_t input_; uint8_t input_;
Command command_; Command command_;
std::optional<uint8_t> keyboard_command_;
ByteQueue<0> output_;
uint8_t last_output_ = 0xff;
bool has_input_ = false; bool has_input_ = false;
bool has_command_ = false; bool has_command_ = false;
std::array<uint8_t, 8> output_queue_;
void advance_output_queue_pointer(size_t &pointer) {
pointer = (pointer + 1) % output_queue_.size();
}
size_t output_read_ = 0, output_write_ = 0;
bool has_output() const {
return output_read_ != output_write_;
}
// bit 7 = 0 keyboard inhibited // bit 7 = 0 keyboard inhibited
// bit 6 = 0 CGA, else MDA // bit 6 = 0 CGA, else MDA
// bit 5 = 0 manufacturing jumper installed // bit 5 = 0 manufacturing jumper installed
@@ -415,42 +458,83 @@ private:
int perform_delay_ = 0; int perform_delay_ = 0;
bool is_tested_ = false; bool is_tested_ = false;
bool enabled_ = false;
enum class Phase { enum class Phase {
Command, Command,
Data, Data,
} phase_ = Phase::Command; } phase_ = Phase::Data;
struct Keyboard { struct Keyboard {
Keyboard(KeyboardController<model> &controller) : controller_(controller) {} // TODO: this is the aped interface for receiving key events from the underlying PC,
// hastily added to align with that for the XT controller. A better interface is needed.
void post(const uint8_t value) { // Not least because of the nonsense fiction here: delivering XT-converted keypresses
controller_.post_keyboard({value}); // directly from an AT keyboard.
void post(const uint8_t key_change) {
output_.append({key_change});
} }
void perform(const uint8_t command) { void perform(const uint8_t command) {
// TODO: delay needed here.
switch(command) { switch(command) {
default:
log_.error().append("Unimplemented keyboard command: %02x", command);
return;
// case 0xed:
// // TODO: use next incoming byte to set LEDs.
// output_.append({0xfa});
// break;
case 0xf2: case 0xf2:
controller_.post_keyboard({0xfa}); output_.append({0xfa, 0x83, 0xab});
break; break;
case 0xff: case 0xff:
controller_.post_keyboard({0xfa, 0xaa}); output_.append({0xfa, 0xaa});
// should_log = true;
break; break;
} }
} }
auto &output() {
return output_;
}
const auto &output() const {
return output_;
}
bool run_for(const int instructions) {
return output_.run_for(instructions);
}
private: private:
KeyboardController<model> &controller_; Log::Logger<Log::Source::Keyboard> log_;
ByteQueue<50> output_;
} keyboard_; } keyboard_;
friend Keyboard; bool has_output() const {
void post_keyboard(const std::initializer_list<uint8_t> values) { return !output_.empty();
if(!enabled_) return; }
for(const auto value : values) {
transmit(value); uint8_t next_output() {
if(!output_.empty()) {
return output_.next();
} }
// Should be unreachable.
return 0xff;
}
void check_irqs() {
bool new_irq1 = false;
if(output_.empty() && !(control_ & Control::InhibitKeyboard) && !keyboard_.output().empty()) {
output_.append({keyboard_.output().next()});
new_irq1 = control_ & Control::AllowKeyboardInterrupts;
}
pics_.pic[0].template apply_edge<1>(new_irq1);
log_.info().append("IRQ1: %d", new_irq1);
} }
}; };

View File

@@ -908,7 +908,12 @@ public:
// Signal interrupt. // Signal interrupt.
context_.flow_controller.unhalt(); context_.flow_controller.unhalt();
InstructionSet::x86::fault(Exception::interrupt(pics_.pic[0].acknowledge()), context_); const auto interrupt_id = pics_.pic[0].acknowledge();
InstructionSet::x86::fault(Exception::interrupt(interrupt_id), context_);
if(should_log) {
log.info().append("Taking interrupt vector %d", interrupt_id);
}
} }
// Do nothing if currently halted. // Do nothing if currently halted.
@@ -951,24 +956,25 @@ public:
// should_log = (decoded_ip_ >= 0x21d0 && decoded_ip_ < 0x221c); // should_log = (decoded_ip_ >= 0x21d0 && decoded_ip_ < 0x221c);
if(should_log) { if(should_log) {
const auto next = to_string(decoded_, InstructionSet::x86::Model::i8086); log.info().append(
static std::string previous; "%04x %s \t\t[ds:6Bh]:%02x",
if(next != previous) { decoded_ip_,
std::cout << std::hex << decoded_ip_ << " " << next; to_string(decoded_, InstructionSet::x86::Model::i80286).c_str(),
// context_.registers.bl()
if(decoded_.second.operation() == InstructionSet::x86::Operation::INT) { context_.memory.template access<uint8_t, InstructionSet::x86::AccessType::PreauthorisedRead>(
std::cout << " dl:" << std::hex << +context_.registers.dl() << "; "; InstructionSet::x86::Source::DS,
std::cout << "ah:" << std::hex << +context_.registers.ah() << "; "; uint16_t(0x6b)
std::cout << "ch:" << std::hex << +context_.registers.ch() << "; "; )
std::cout << "cl:" << std::hex << +context_.registers.cl() << "; "; ).append_if(decoded_.second.operation() == InstructionSet::x86::Operation::INT,
std::cout << "dh:" << std::hex << +context_.registers.dh() << "; "; " dl:%02x ah:%02x ch:%02x cl:%02x dh:%02x es:%04x bx:%04x",
std::cout << "es:" << std::hex << +context_.registers.es() << "; "; context_.registers.dl(),
std::cout << "bx:" << std::hex << +context_.registers.bx(); context_.registers.ah(),
} context_.registers.ch(),
context_.registers.cl(),
std::cout << std::endl; context_.registers.dh(),
previous = next; context_.registers.es(),
} context_.registers.bx()
);
} }
if(decoded_.second.operation() == InstructionSet::x86::Operation::Invalid) { if(decoded_.second.operation() == InstructionSet::x86::Operation::Invalid) {
@@ -1020,7 +1026,7 @@ public:
} }
void set_key_state(const uint16_t key, const bool is_pressed) final { void set_key_state(const uint16_t key, const bool is_pressed) final {
keyboard_.keyboard().post(uint8_t(key | (is_pressed ? 0x00 : 0x80))); keyboard_.post_keyboard(uint8_t(key | (is_pressed ? 0x00 : 0x80)));
} }
// MARK: - Activity::Source. // MARK: - Activity::Source.

View File

@@ -9,11 +9,13 @@
#pragma once #pragma once
#include "Analyser/Static/PCCompatible/Target.hpp" #include "Analyser/Static/PCCompatible/Target.hpp"
#include "Outputs/Log.hpp"
namespace PCCompatible { namespace PCCompatible {
// Cf. https://helppc.netcore2k.net/hardware/pic // Cf. https://helppc.netcore2k.net/hardware/pic
class PIC { class PIC {
using Log = Log::Logger<Log::Source::PIC>;
public: public:
template <int address> template <int address>
void write(const uint8_t value) { void write(const uint8_t value) {
@@ -42,6 +44,7 @@ public:
} }
} else { } else {
mask_ = value; mask_ = value;
Log::info().append("Mask set to %02x; requests now %02x", mask_, requests_);
} }
} else { } else {
if(value & 0x10) { if(value & 0x10) {
@@ -59,6 +62,8 @@ public:
single_pic_ = value & 2; single_pic_ = value & 2;
four_byte_vectors_ = value & 4; four_byte_vectors_ = value & 4;
level_triggered_ = value & 8; level_triggered_ = value & 8;
Log::info().append("Level triggered: %d", level_triggered_);
} else if(value & 0x08) { } else if(value & 0x08) {
// //
// Operation Control Word 3. // Operation Control Word 3.
@@ -78,7 +83,7 @@ public:
// b2, b1, b0: interrupt level to acknowledge. // b2, b1, b0: interrupt level to acknowledge.
switch(value >> 5) { switch(value >> 5) {
default: default:
printf("PIC: TODO EOI type %d\n", value >> 5); Log::error().append("PIC: TODO EOI type %d\n", value >> 5);
[[fallthrough]]; [[fallthrough]];
case 0b010: // No-op. case 0b010: // No-op.
break; break;
@@ -109,21 +114,26 @@ public:
if(address) { if(address) {
return mask_; return mask_;
} }
return 0;
Log::error().append("Reading address 0");
return requests_;
} }
template <int input> template <int input>
void apply_edge(const bool final_level) { void apply_edge(const bool final_level) {
constexpr uint8_t input_mask = 1 << input; static constexpr uint8_t input_mask = 1 << input;
const auto old_levels = levels_;
const uint8_t new_bit = final_level ? input_mask : 0;
levels_ = (levels_ & ~input_mask) | new_bit;
// Guess: level triggered means the request can be forwarded only so long as the
// relevant input is actually high. Whereas edge triggered implies capturing state.
if(level_triggered_) { if(level_triggered_) {
requests_ &= ~input_mask; requests_ = levels_;
} } else {
if(final_level) { requests_ |= (levels_ ^ old_levels) & new_bit;
requests_ |= input_mask;
} }
Log::info().append("%d to %d => requests now %02x", input, final_level, requests_);
} }
bool pending() const { bool pending() const {
@@ -143,6 +153,7 @@ public:
eoi_target_ = id; eoi_target_ = id;
awaiting_eoi_ = !auto_eoi_; awaiting_eoi_ = !auto_eoi_;
requests_ &= ~in_service_; requests_ &= ~in_service_;
Log::info().append("Implicitly acknowledging: %d; requests now: %02x", id, requests_);
return uint8_t(vector_base_ + id); return uint8_t(vector_base_ + id);
} }
@@ -163,6 +174,7 @@ private:
uint8_t requests_ = 0; uint8_t requests_ = 0;
uint8_t in_service_ = 0; uint8_t in_service_ = 0;
uint8_t levels_ = 0;
struct ConfgurationState { struct ConfgurationState {
int word; int word;

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

@@ -56,6 +56,8 @@ enum class Source {
OpenGL, OpenGL,
PCCompatible, PCCompatible,
PCPOST, PCPOST,
PIC,
PIT,
Plus4, Plus4,
PCMTrack, PCMTrack,
SCC, SCC,
@@ -76,7 +78,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.
@@ -96,19 +98,20 @@ constexpr EnabledLevel enabled_level(const Source source) {
case Source::SCC: case Source::SCC:
case Source::SCSI: case Source::SCSI:
case Source::I2C: case Source::I2C:
// case Source::PCPOST:
return EnabledLevel::None; return EnabledLevel::None;
case Source::Floppy: case Source::Floppy:
// case Source::Keyboard:
return EnabledLevel::Errors; return EnabledLevel::Errors;
} }
} }
constexpr const char *prefix(const Source source) { constexpr const char *prefix(const Source source) {
switch(source) { switch(source) {
default: return nullptr;
case Source::ADBDevice: return "ADB device"; case Source::ADBDevice: return "ADB device";
case Source::ADBGLU: return "ADB GLU"; case Source::ADBGLU: return "ADB GLU";
case Source::Amiga: return "Amiga";
case Source::AmigaBlitter: return "Blitter"; case Source::AmigaBlitter: return "Blitter";
case Source::AmigaChipset: return "Chipset"; case Source::AmigaChipset: return "Chipset";
case Source::AmigaCopper: return "Copper"; case Source::AmigaCopper: return "Copper";
@@ -143,31 +146,80 @@ constexpr const char *prefix(const Source source) {
case Source::Plus4: return "Plus4"; case Source::Plus4: return "Plus4";
case Source::PCCompatible: return "PC"; case Source::PCCompatible: return "PC";
case Source::PCPOST: return "POST"; case Source::PCPOST: return "POST";
case Source::PIC: return "PIC";
case Source::PIT: return "PIT";
case Source::PCMTrack: return "PCM Track"; case Source::PCMTrack: return "PCM Track";
case Source::SCSI: return "SCSI"; case Source::SCSI: return "SCSI";
case Source::SCC: return "SCC"; case Source::SCC: return "SCC";
case Source::SZX: return "SZX"; case Source::SZX: return "SZX";
case Source::TapeUEF: return "UEF"; case Source::TapeUEF: return "UEF";
case Source::TMS9918: return "TMS9918";
case Source::TZX: return "TZX"; case Source::TZX: return "TZX";
case Source::Vic20: return "Vic20"; case Source::Vic20: return "Vic20";
case Source::WDFDC: return "WD FDC"; case Source::WDFDC: return "WD FDC";
} }
return nullptr;
} }
template <Source source, bool enabled> template <Source source, bool enabled>
struct LogLine; struct LogLine;
struct RepeatAccumulator {
std::string last;
Source source;
size_t count = 0;
FILE *stream;
};
struct AccumulatingLog {
inline static thread_local RepeatAccumulator accumulator_;
};
template <Source source> template <Source source>
struct LogLine<source, true> { struct LogLine<source, true>: private AccumulatingLog {
public: public:
explicit LogLine(FILE *const stream) noexcept : stream_(stream) { explicit LogLine(FILE *const stream) noexcept :
static constexpr auto source_prefix = prefix(source); stream_(stream) {}
if(!source_prefix) return;
append("[%s] ", source_prefix);
}
~LogLine() { ~LogLine() {
fprintf(stream_, "%s\n", output_.c_str()); if(output_ == accumulator_.last && source == accumulator_.source && stream_ == accumulator_.stream) {
++accumulator_.count;
return;
}
if(!accumulator_.last.empty()) {
const char *const unadorned_prefix = prefix(accumulator_.source);
std::string prefix;
if(unadorned_prefix) {
prefix = "[";
prefix += unadorned_prefix;
prefix += "] ";
}
if(accumulator_.count > 1) {
fprintf(
accumulator_.stream,
"%s%s [* %zu]\n",
prefix.c_str(),
accumulator_.last.c_str(),
accumulator_.count
);
} else {
fprintf(
accumulator_.stream,
"%s%s\n",
prefix.c_str(),
accumulator_.last.c_str()
);
}
}
accumulator_.count = 1;
accumulator_.last = output_;
accumulator_.source = source;
accumulator_.stream = stream_;
} }
template <size_t size, typename... Args> template <size_t size, typename... Args>
@@ -190,8 +242,8 @@ public:
} }
private: private:
std::string output_;
FILE *stream_; FILE *stream_;
std::string output_;
}; };
template <Source source> template <Source source>

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