1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-08-14 23:27:26 +00:00

Merge pull request #1428 from TomHarte/MoreIndentation

Adjust more dangling indentation changes.
This commit is contained in:
Thomas Harte
2024-12-04 22:40:30 -05:00
committed by GitHub
45 changed files with 5530 additions and 5528 deletions

View File

@@ -70,7 +70,7 @@ private:
uint8_t track_; uint8_t track_;
std::shared_ptr<Sector> sector_cache_[65536]; std::shared_ptr<Sector> sector_cache_[65536];
void process_input_bit(const int value) { void process_input_bit(const int value) override {
shift_register_ = ((shift_register_ << 1) | unsigned(value)) & 0x3ff; shift_register_ = ((shift_register_ << 1) | unsigned(value)) & 0x3ff;
bit_count_++; bit_count_++;
} }
@@ -107,7 +107,7 @@ private:
} }
} }
void process_index_hole() { void process_index_hole() override {
index_count_++; index_count_++;
} }

View File

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

View File

@@ -19,67 +19,67 @@
namespace Apple::IIgs::ADB { namespace Apple::IIgs::ADB {
class GLU: public InstructionSet::M50740::PortHandler { class GLU: public InstructionSet::M50740::PortHandler {
public: public:
GLU(); GLU();
uint8_t get_keyboard_data(); uint8_t get_keyboard_data();
uint8_t get_mouse_data(); uint8_t get_mouse_data();
uint8_t get_modifier_status(); uint8_t get_modifier_status();
uint8_t get_any_key_down(); uint8_t get_any_key_down();
uint8_t get_data(); uint8_t get_data();
uint8_t get_status(); uint8_t get_status();
void set_command(uint8_t); void set_command(uint8_t);
void set_status(uint8_t); void set_status(uint8_t);
void clear_key_strobe(); void clear_key_strobe();
void set_microcontroller_rom(const std::vector<uint8_t> &rom); void set_microcontroller_rom(const std::vector<uint8_t> &rom);
void run_for(Cycles cycles); void run_for(Cycles cycles);
bool get_command_button() const; bool get_command_button() const;
bool get_option_button() const; bool get_option_button() const;
void set_vertical_blank(bool); void set_vertical_blank(bool);
bool get_vertical_blank() { bool get_vertical_blank() {
return vertical_blank_; return vertical_blank_;
} }
Apple::ADB::Keyboard &keyboard() { Apple::ADB::Keyboard &keyboard() {
return keyboard_; return keyboard_;
} }
Inputs::Mouse &get_mouse() { Inputs::Mouse &get_mouse() {
return mouse_; return mouse_;
} }
private: private:
InstructionSet::M50740::Executor executor_; InstructionSet::M50740::Executor executor_;
void run_ports_for(Cycles) override; void run_ports_for(Cycles) override;
void set_port_output(int port, uint8_t value) override; void set_port_output(int port, uint8_t value) override;
uint8_t get_port_input(int port) override; uint8_t get_port_input(int port) override;
uint8_t registers_[16]{}; uint8_t registers_[16]{};
uint8_t register_address_; uint8_t register_address_;
uint8_t register_latch_ = 0xff; uint8_t register_latch_ = 0xff;
bool register_strobe_ = false; bool register_strobe_ = false;
uint8_t status_ = 0x00; uint8_t status_ = 0x00;
Apple::ADB::Bus bus_; Apple::ADB::Bus bus_;
size_t controller_id_; size_t controller_id_;
uint8_t modifier_state_ = 0; uint8_t modifier_state_ = 0;
bool vertical_blank_ = false; bool vertical_blank_ = false;
int visible_mouse_register_ = 2; int visible_mouse_register_ = 2;
// For now, attach only a keyboard and mouse. // For now, attach only a keyboard and mouse.
Apple::ADB::Mouse mouse_; Apple::ADB::Mouse mouse_;
Apple::ADB::Keyboard keyboard_; Apple::ADB::Keyboard keyboard_;
}; };
} }

File diff suppressed because it is too large Load Diff

View File

@@ -20,144 +20,144 @@
namespace Apple::IIgs { namespace Apple::IIgs {
class MemoryMap { class MemoryMap {
public: public:
// MARK: - Initial construction and configuration. // MARK: - Initial construction and configuration.
MemoryMap(bool is_rom03) : auxiliary_switches_(*this), language_card_(*this) { MemoryMap(bool is_rom03) : auxiliary_switches_(*this), language_card_(*this) {
setup_shadow_maps(is_rom03); setup_shadow_maps(is_rom03);
} }
/// Sets the ROM and RAM storage underlying this MemoryMap. /// Sets the ROM and RAM storage underlying this MemoryMap.
void set_storage(std::vector<uint8_t> &ram, std::vector<uint8_t> &rom); void set_storage(std::vector<uint8_t> &ram, std::vector<uint8_t> &rom);
// MARK: - Live bus access notifications and register access. // MARK: - Live bus access notifications and register access.
void set_shadow_register(uint8_t value); void set_shadow_register(uint8_t value);
uint8_t get_shadow_register() const; uint8_t get_shadow_register() const;
void set_speed_register(uint8_t value); void set_speed_register(uint8_t value);
void set_state_register(uint8_t value); void set_state_register(uint8_t value);
uint8_t get_state_register() const; uint8_t get_state_register() const;
void access(uint16_t address, bool is_read); void access(uint16_t address, bool is_read);
using AuxiliaryMemorySwitches = Apple::II::AuxiliaryMemorySwitches<MemoryMap>; using AuxiliaryMemorySwitches = Apple::II::AuxiliaryMemorySwitches<MemoryMap>;
const AuxiliaryMemorySwitches &auxiliary_switches() const { const AuxiliaryMemorySwitches &auxiliary_switches() const {
return auxiliary_switches_; return auxiliary_switches_;
} }
using LanguageCardSwitches = Apple::II::LanguageCardSwitches<MemoryMap>; using LanguageCardSwitches = Apple::II::LanguageCardSwitches<MemoryMap>;
const LanguageCardSwitches &language_card_switches() const { const LanguageCardSwitches &language_card_switches() const {
return language_card_; return language_card_;
} }
// MARK: - Accessors for reading and writing RAM. // MARK: - Accessors for reading and writing RAM.
struct Region { struct Region {
uint8_t *write = nullptr; uint8_t *write = nullptr;
const uint8_t *read = nullptr; const uint8_t *read = nullptr;
uint8_t flags = 0; uint8_t flags = 0;
enum Flag: uint8_t { enum Flag: uint8_t {
Is1Mhz = 1 << 0, // Both reads and writes should be synchronised with the 1Mhz clock. Is1Mhz = 1 << 0, // Both reads and writes should be synchronised with the 1Mhz clock.
IsIO = 1 << 1, // Indicates that this region should be checked for soft switches, registers, etc. IsIO = 1 << 1, // Indicates that this region should be checked for soft switches, registers, etc.
};
}; };
};
const Region &region(uint32_t address) const { return regions_[region_map_[address >> 8]]; } const Region &region(uint32_t address) const { return regions_[region_map_[address >> 8]]; }
uint8_t read(const Region &region, uint32_t address) const { uint8_t read(const Region &region, uint32_t address) const {
return region.read ? region.read[address] : 0xff; return region.read ? region.read[address] : 0xff;
}
bool is_shadowed(const Region &region, uint32_t address) const {
// ROM is never shadowed.
if(!region.write) {
return false;
} }
bool is_shadowed(const Region &region, uint32_t address) const { const auto physical = physical_address(region, address);
// ROM is never shadowed. assert(physical <= 0xff'ffff);
if(!region.write) { return shadow_pages_[(physical >> 10) & 127] && shadow_banks_[physical >> 17];
return false; }
} void write(const Region &region, uint32_t address, uint8_t value) {
if(!region.write) {
const auto physical = physical_address(region, address); return;
assert(physical <= 0xff'ffff);
return shadow_pages_[(physical >> 10) & 127] && shadow_banks_[physical >> 17];
}
void write(const Region &region, uint32_t address, uint8_t value) {
if(!region.write) {
return;
}
// Write once.
region.write[address] = value;
// Write again, either to the same place (if unshadowed) or to the shadow destination.
static constexpr std::size_t shadow_mask[2] = {0xff'ffff, 0x01'ffff};
const bool shadowed = is_shadowed(region, address);
shadow_base_[shadowed][physical_address(region, address) & shadow_mask[shadowed]] = value;
} }
// The objective is to support shadowing: // Write once.
// 1. without storing a whole extra pointer, and such that the shadowing flags region.write[address] = value;
// are orthogonal to the current auxiliary memory settings;
// 2. in such a way as to support shadowing both in banks $00/$01 and elsewhere; and
// 3. to do so without introducing too much in the way of branching.
//
// Hence the implemented solution: if shadowing is enabled then use the distance from the start of
// physical RAM modulo 128k indexed into the bank $e0/$e1 RAM.
//
// With a further twist: the modulo and pointer are indexed on ::IsShadowed to eliminate a branch
// even on that.
private: // Write again, either to the same place (if unshadowed) or to the shadow destination.
AuxiliaryMemorySwitches auxiliary_switches_; static constexpr std::size_t shadow_mask[2] = {0xff'ffff, 0x01'ffff};
LanguageCardSwitches language_card_; const bool shadowed = is_shadowed(region, address);
friend AuxiliaryMemorySwitches; shadow_base_[shadowed][physical_address(region, address) & shadow_mask[shadowed]] = value;
friend LanguageCardSwitches; }
uint8_t shadow_register_ = 0x00; // The objective is to support shadowing:
uint8_t speed_register_ = 0x00; // 1. without storing a whole extra pointer, and such that the shadowing flags
// are orthogonal to the current auxiliary memory settings;
// 2. in such a way as to support shadowing both in banks $00/$01 and elsewhere; and
// 3. to do so without introducing too much in the way of branching.
//
// Hence the implemented solution: if shadowing is enabled then use the distance from the start of
// physical RAM modulo 128k indexed into the bank $e0/$e1 RAM.
//
// With a further twist: the modulo and pointer are indexed on ::IsShadowed to eliminate a branch
// even on that.
// MARK: - Banking. private:
AuxiliaryMemorySwitches auxiliary_switches_;
LanguageCardSwitches language_card_;
friend AuxiliaryMemorySwitches;
friend LanguageCardSwitches;
void assert_is_region(uint8_t start, uint8_t end); uint8_t shadow_register_ = 0x00;
template <int type> void set_paging(); uint8_t speed_register_ = 0x00;
uint8_t *ram_base_ = nullptr; // MARK: - Banking.
// Memory layout here is done via double indirection; the main loop should: void assert_is_region(uint8_t start, uint8_t end);
// (i) use the top two bytes of the address to get an index from region_map; and template <int type> void set_paging();
// (ii) use that to index the memory_regions table.
//
// Pointers are eight bytes at the time of writing, so the extra level of indirection
// reduces what would otherwise be a 1.25mb table down to not a great deal more than 64kb.
std::array<uint8_t, 65536> region_map_{};
std::array<Region, 40> regions_; // An assert above ensures that this is large enough; there's no
// doctrinal reason for it to be whatever size it is now, just
// adjust as required.
std::size_t physical_address(const Region &region, uint32_t address) const { uint8_t *ram_base_ = nullptr;
return std::size_t(&region.write[address] - ram_base_);
}
// MARK: - Shadowing // Memory layout here is done via double indirection; the main loop should:
// (i) use the top two bytes of the address to get an index from region_map; and
// (ii) use that to index the memory_regions table.
//
// Pointers are eight bytes at the time of writing, so the extra level of indirection
// reduces what would otherwise be a 1.25mb table down to not a great deal more than 64kb.
std::array<uint8_t, 65536> region_map_{};
std::array<Region, 40> regions_; // An assert above ensures that this is large enough; there's no
// doctrinal reason for it to be whatever size it is now, just
// adjust as required.
// Various precomputed bitsets describing key regions; std::bitset doesn't support constexpr instantiation std::size_t physical_address(const Region &region, uint32_t address) const {
// beyond the first 64 bits at the time of writing, alas, so these are generated at runtime. return std::size_t(&region.write[address] - ram_base_);
std::bitset<128> shadow_text1_; }
std::bitset<128> shadow_text2_;
std::bitset<128> shadow_highres1_, shadow_highres1_aux_;
std::bitset<128> shadow_highres2_, shadow_highres2_aux_;
std::bitset<128> shadow_superhighres_;
void setup_shadow_maps(bool is_rom03);
void set_shadowing();
uint8_t *shadow_base_[2] = {nullptr, nullptr}; // MARK: - Shadowing
// Divide the final 128kb of memory into 1kb chunks and flag to indicate whether // Various precomputed bitsets describing key regions; std::bitset doesn't support constexpr instantiation
// each is a potential destination for shadowing. // beyond the first 64 bits at the time of writing, alas, so these are generated at runtime.
std::bitset<128> shadow_pages_{}; std::bitset<128> shadow_text1_;
std::bitset<128> shadow_text2_;
std::bitset<128> shadow_highres1_, shadow_highres1_aux_;
std::bitset<128> shadow_highres2_, shadow_highres2_aux_;
std::bitset<128> shadow_superhighres_;
void setup_shadow_maps(bool is_rom03);
void set_shadowing();
// Divide the whole 16mb of memory into 128kb chunks and flag to indicate whether uint8_t *shadow_base_[2] = {nullptr, nullptr};
// each is a potential source of shadowing.
std::bitset<128> shadow_banks_{}; // Divide the final 128kb of memory into 1kb chunks and flag to indicate whether
// each is a potential destination for shadowing.
std::bitset<128> shadow_pages_{};
// Divide the whole 16mb of memory into 128kb chunks and flag to indicate whether
// each is a potential source of shadowing.
std::bitset<128> shadow_banks_{};
}; };
} }

View File

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

View File

@@ -20,236 +20,236 @@ namespace Apple::IIgs::Video {
stretched cycle. stretched cycle.
*/ */
class Video: public Apple::II::VideoSwitches<Cycles> { class Video: public Apple::II::VideoSwitches<Cycles> {
public:
Video();
void set_internal_ram(const uint8_t *);
bool get_is_vertical_blank(Cycles offset);
uint8_t get_horizontal_counter(Cycles offset);
uint8_t get_vertical_counter(Cycles offset);
void set_new_video(uint8_t);
uint8_t get_new_video();
void clear_interrupts(uint8_t);
uint8_t get_interrupt_register();
void set_interrupt_register(uint8_t);
bool get_interrupt_line();
void notify_clock_tick();
void set_border_colour(uint8_t);
void set_text_colour(uint8_t);
uint8_t get_text_colour();
uint8_t get_border_colour();
void set_composite_is_colour(bool);
bool get_composite_is_colour();
/// Sets the scan target.
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
/// Gets the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const;
/// Sets the type of output.
void set_display_type(Outputs::Display::DisplayType);
/// Gets the type of output.
Outputs::Display::DisplayType get_display_type() const;
/// Determines the period until video might autonomously update its interrupt lines.
Cycles next_sequence_point() const;
/// Sets the Mega II interrupt enable state — 1/4-second and VBL interrupts are
/// generated here.
void set_megaii_interrupts_enabled(uint8_t);
uint8_t get_megaii_interrupt_status();
void clear_megaii_interrupts();
private:
Outputs::CRT::CRT crt_;
// This is coupled to Apple::II::GraphicsMode, but adds detail for the IIgs.
enum class GraphicsMode {
Text = 0,
DoubleText,
HighRes,
DoubleHighRes,
LowRes,
DoubleLowRes,
FatLowRes,
// Additions:
DoubleHighResMono,
SuperHighRes
};
constexpr bool is_colour_ntsc(const GraphicsMode m) {
return m >= GraphicsMode::HighRes && m <= GraphicsMode::FatLowRes;
}
GraphicsMode graphics_mode(int row) const {
if(new_video_ & 0x80) {
return GraphicsMode::SuperHighRes;
}
const auto ii_mode = Apple::II::VideoSwitches<Cycles>::graphics_mode(row);
switch(ii_mode) {
// Coupling very much assumed here.
case Apple::II::GraphicsMode::DoubleHighRes:
if(new_video_ & 0x20) {
return GraphicsMode::DoubleHighResMono;
}
[[fallthrough]];
default: return GraphicsMode(int(ii_mode)); break;
}
}
enum class PixelBufferFormat {
Text, DoubleText, NTSC, NTSCMono, SuperHighRes
};
constexpr PixelBufferFormat format_for_mode(GraphicsMode m) {
switch(m) {
case GraphicsMode::Text: return PixelBufferFormat::Text;
case GraphicsMode::DoubleText: return PixelBufferFormat::DoubleText;
default: return PixelBufferFormat::NTSC;
case GraphicsMode::DoubleHighResMono: return PixelBufferFormat::NTSCMono;
case GraphicsMode::SuperHighRes: return PixelBufferFormat::SuperHighRes;
}
}
void advance(Cycles);
uint8_t new_video_ = 0x01;
class Interrupts {
public: public:
Video(); void add(uint8_t value) {
void set_internal_ram(const uint8_t *); // Taken literally, status accumulates regardless of being enabled,
// potentially to be polled, it simply doesn't trigger an interrupt.
value_ |= value;
test();
}
bool get_is_vertical_blank(Cycles offset); void clear(uint8_t value) {
uint8_t get_horizontal_counter(Cycles offset); // Zeroes in bits 5 or 6 clear the respective interrupts.
uint8_t get_vertical_counter(Cycles offset); value_ &= value | ~0x60;
test();
}
void set_new_video(uint8_t); void set_control(uint8_t value) {
uint8_t get_new_video(); // Ones in bits 1 or 2 enable the respective interrupts.
value_ = (value_ & ~0x6) | (value & 0x6);
test();
}
void clear_interrupts(uint8_t); uint8_t status() const {
uint8_t get_interrupt_register(); return value_;
void set_interrupt_register(uint8_t); }
bool get_interrupt_line();
void notify_clock_tick(); bool active() const {
return value_ & 0x80;
void set_border_colour(uint8_t); }
void set_text_colour(uint8_t);
uint8_t get_text_colour();
uint8_t get_border_colour();
void set_composite_is_colour(bool);
bool get_composite_is_colour();
/// Sets the scan target.
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
/// Gets the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const;
/// Sets the type of output.
void set_display_type(Outputs::Display::DisplayType);
/// Gets the type of output.
Outputs::Display::DisplayType get_display_type() const;
/// Determines the period until video might autonomously update its interrupt lines.
Cycles next_sequence_point() const;
/// Sets the Mega II interrupt enable state — 1/4-second and VBL interrupts are
/// generated here.
void set_megaii_interrupts_enabled(uint8_t);
uint8_t get_megaii_interrupt_status();
void clear_megaii_interrupts();
private: private:
Outputs::CRT::CRT crt_; void test() {
value_ &= 0x7f;
// This is coupled to Apple::II::GraphicsMode, but adds detail for the IIgs. if((value_ >> 4) & value_ & 0x6) {
enum class GraphicsMode { value_ |= 0x80;
Text = 0,
DoubleText,
HighRes,
DoubleHighRes,
LowRes,
DoubleLowRes,
FatLowRes,
// Additions:
DoubleHighResMono,
SuperHighRes
};
constexpr bool is_colour_ntsc(const GraphicsMode m) {
return m >= GraphicsMode::HighRes && m <= GraphicsMode::FatLowRes;
}
GraphicsMode graphics_mode(int row) const {
if(new_video_ & 0x80) {
return GraphicsMode::SuperHighRes;
}
const auto ii_mode = Apple::II::VideoSwitches<Cycles>::graphics_mode(row);
switch(ii_mode) {
// Coupling very much assumed here.
case Apple::II::GraphicsMode::DoubleHighRes:
if(new_video_ & 0x20) {
return GraphicsMode::DoubleHighResMono;
}
[[fallthrough]];
default: return GraphicsMode(int(ii_mode)); break;
} }
} }
enum class PixelBufferFormat { // Overall meaning of value is as per the VGC interrupt register, i.e.
Text, DoubleText, NTSC, NTSCMono, SuperHighRes
};
constexpr PixelBufferFormat format_for_mode(GraphicsMode m) {
switch(m) {
case GraphicsMode::Text: return PixelBufferFormat::Text;
case GraphicsMode::DoubleText: return PixelBufferFormat::DoubleText;
default: return PixelBufferFormat::NTSC;
case GraphicsMode::DoubleHighResMono: return PixelBufferFormat::NTSCMono;
case GraphicsMode::SuperHighRes: return PixelBufferFormat::SuperHighRes;
}
}
void advance(Cycles);
uint8_t new_video_ = 0x01;
class Interrupts {
public:
void add(uint8_t value) {
// Taken literally, status accumulates regardless of being enabled,
// potentially to be polled, it simply doesn't trigger an interrupt.
value_ |= value;
test();
}
void clear(uint8_t value) {
// Zeroes in bits 5 or 6 clear the respective interrupts.
value_ &= value | ~0x60;
test();
}
void set_control(uint8_t value) {
// Ones in bits 1 or 2 enable the respective interrupts.
value_ = (value_ & ~0x6) | (value & 0x6);
test();
}
uint8_t status() const {
return value_;
}
bool active() const {
return value_ & 0x80;
}
private:
void test() {
value_ &= 0x7f;
if((value_ >> 4) & value_ & 0x6) {
value_ |= 0x80;
}
}
// Overall meaning of value is as per the VGC interrupt register, i.e.
//
// b7: interrupt status;
// b6: 1-second interrupt status;
// b5: scan-line interrupt status;
// b4: reserved;
// b3: reserved;
// b2: 1-second interrupt enable;
// b1: scan-line interrupt enable;
// b0: reserved.
uint8_t value_ = 0x00;
} interrupts_;
int cycles_into_frame_ = 0;
const uint8_t *ram_ = nullptr;
// The modal colours.
uint16_t border_colour_ = 0;
uint8_t border_colour_entry_ = 0;
uint8_t text_colour_entry_ = 0xf0;
uint16_t text_colour_ = 0xffff;
uint16_t background_colour_ = 0;
// Current pixel output buffer and conceptual format.
PixelBufferFormat pixels_format_;
uint16_t *pixels_ = nullptr, *next_pixel_ = nullptr;
int pixels_start_column_;
void output_row(int row, int start, int end);
uint16_t *output_super_high_res(uint16_t *target, int start, int end, int row) const;
uint16_t *output_text(uint16_t *target, int start, int end, int row) const;
uint16_t *output_double_text(uint16_t *target, int start, int end, int row) const;
uint16_t *output_char(uint16_t *target, uint8_t source, int row) const;
uint16_t *output_low_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_fat_low_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_double_low_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_high_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_double_high_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_double_high_resolution_mono(uint16_t *target, int start, int end, int row);
// Super high-res per-line state.
uint8_t line_control_;
uint16_t palette_[16];
// Storage used for fill mode.
uint16_t *palette_zero_[4] = {nullptr, nullptr, nullptr, nullptr}, palette_throwaway_;
// Lookup tables and state to assist in the IIgs' mapping from NTSC to RGB.
// //
// My understanding of the real-life algorithm is: maintain a four-bit buffer. // b7: interrupt status;
// Fill it in a circular fashion. Ordinarily, output the result of looking // b6: 1-second interrupt status;
// up the RGB mapping of those four bits of Apple II output (which outputs four // b5: scan-line interrupt status;
// bits per NTSC colour cycle), commuted as per current phase. But if the bit // b4: reserved;
// being inserted differs from that currently in its position in the shift // b3: reserved;
// register, hold the existing output for three shifts. // b2: 1-second interrupt enable;
// // b1: scan-line interrupt enable;
// From there I am using the following: // b0: reserved.
uint8_t value_ = 0x00;
} interrupts_;
// Maps from: int cycles_into_frame_ = 0;
// const uint8_t *ram_ = nullptr;
// b0 = b0 of the shift register
// b1 = b4 of the shift register
// b2 = current delay count
//
// to a new delay count.
uint8_t ntsc_delay_lookup_[20];
uint32_t ntsc_shift_ = 0; // Assumption here: logical shifts will ensue, rather than arithmetic.
int ntsc_delay_ = 0;
/// Outputs the lowest 14 bits from @c ntsc_shift_, mapping to RGB. // The modal colours.
/// Phase is derived from @c column. uint16_t border_colour_ = 0;
uint16_t *output_shift(uint16_t *target, int column); uint8_t border_colour_entry_ = 0;
uint8_t text_colour_entry_ = 0xf0;
uint16_t text_colour_ = 0xffff;
uint16_t background_colour_ = 0;
// Common getter for the two counters. // Current pixel output buffer and conceptual format.
struct Counters { PixelBufferFormat pixels_format_;
Counters(int v, int h) : vertical(v), horizontal(h) {} uint16_t *pixels_ = nullptr, *next_pixel_ = nullptr;
const int vertical, horizontal; int pixels_start_column_;
};
Counters get_counters(Cycles offset);
// Marshalls the Mega II-style interrupt state. void output_row(int row, int start, int end);
uint8_t megaii_interrupt_mask_ = 0;
uint8_t megaii_interrupt_state_ = 0; uint16_t *output_super_high_res(uint16_t *target, int start, int end, int row) const;
int megaii_frame_counter_ = 0; // To count up to quarter-second interrupts.
uint16_t *output_text(uint16_t *target, int start, int end, int row) const;
uint16_t *output_double_text(uint16_t *target, int start, int end, int row) const;
uint16_t *output_char(uint16_t *target, uint8_t source, int row) const;
uint16_t *output_low_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_fat_low_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_double_low_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_high_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_double_high_resolution(uint16_t *target, int start, int end, int row);
uint16_t *output_double_high_resolution_mono(uint16_t *target, int start, int end, int row);
// Super high-res per-line state.
uint8_t line_control_;
uint16_t palette_[16];
// Storage used for fill mode.
uint16_t *palette_zero_[4] = {nullptr, nullptr, nullptr, nullptr}, palette_throwaway_;
// Lookup tables and state to assist in the IIgs' mapping from NTSC to RGB.
//
// My understanding of the real-life algorithm is: maintain a four-bit buffer.
// Fill it in a circular fashion. Ordinarily, output the result of looking
// up the RGB mapping of those four bits of Apple II output (which outputs four
// bits per NTSC colour cycle), commuted as per current phase. But if the bit
// being inserted differs from that currently in its position in the shift
// register, hold the existing output for three shifts.
//
// From there I am using the following:
// Maps from:
//
// b0 = b0 of the shift register
// b1 = b4 of the shift register
// b2 = current delay count
//
// to a new delay count.
uint8_t ntsc_delay_lookup_[20];
uint32_t ntsc_shift_ = 0; // Assumption here: logical shifts will ensue, rather than arithmetic.
int ntsc_delay_ = 0;
/// Outputs the lowest 14 bits from @c ntsc_shift_, mapping to RGB.
/// Phase is derived from @c column.
uint16_t *output_shift(uint16_t *target, int column);
// Common getter for the two counters.
struct Counters {
Counters(int v, int h) : vertical(v), horizontal(h) {}
const int vertical, horizontal;
};
Counters get_counters(Cycles offset);
// Marshalls the Mega II-style interrupt state.
uint8_t megaii_interrupt_mask_ = 0;
uint8_t megaii_interrupt_state_ = 0;
int megaii_frame_counter_ = 0; // To count up to quarter-second interrupts.
}; };
} }

View File

@@ -24,61 +24,61 @@ namespace Apple::Macintosh {
a shade less than 4Mhz. a shade less than 4Mhz.
*/ */
class Audio: public ::Outputs::Speaker::BufferSource<Audio, false> { class Audio: public ::Outputs::Speaker::BufferSource<Audio, false> {
public: public:
Audio(Concurrency::AsyncTaskQueue<false> &task_queue); Audio(Concurrency::AsyncTaskQueue<false> &task_queue);
/*! /*!
Macintosh audio is (partly) sourced by the same scanning Macintosh audio is (partly) sourced by the same scanning
hardware as the video; each line it collects an additional hardware as the video; each line it collects an additional
word of memory, half of which is used for audio output. word of memory, half of which is used for audio output.
Use this method to add a newly-collected sample to the queue. Use this method to add a newly-collected sample to the queue.
*/ */
void post_sample(uint8_t sample); void post_sample(uint8_t sample);
/*! /*!
Macintosh audio also separately receives an output volume Macintosh audio also separately receives an output volume
level, in the range 0 to 7. level, in the range 0 to 7.
Use this method to set the current output volume. Use this method to set the current output volume.
*/ */
void set_volume(int volume); void set_volume(int volume);
/*! /*!
A further factor in audio output is the on-off toggle. A further factor in audio output is the on-off toggle.
*/ */
void set_enabled(bool on); void set_enabled(bool on);
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter. // to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
template <Outputs::Speaker::Action action> template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target); void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
bool is_zero_level() const; bool is_zero_level() const;
void set_sample_volume_range(std::int16_t range); void set_sample_volume_range(std::int16_t range);
private: private:
Concurrency::AsyncTaskQueue<false> &task_queue_; Concurrency::AsyncTaskQueue<false> &task_queue_;
// A queue of fetched samples; read from by one thread, // A queue of fetched samples; read from by one thread,
// written to by another. // written to by another.
struct { struct {
std::array<std::atomic<uint8_t>, 740> buffer; std::array<std::atomic<uint8_t>, 740> buffer;
size_t read_pointer = 0, write_pointer = 0; size_t read_pointer = 0, write_pointer = 0;
} sample_queue_; } sample_queue_;
// Emulator-thread stateful variables, to avoid work posting // Emulator-thread stateful variables, to avoid work posting
// deferral updates if possible. // deferral updates if possible.
int posted_volume_ = 0; int posted_volume_ = 0;
int posted_enable_mask_ = 0; int posted_enable_mask_ = 0;
// Stateful variables, modified from the audio generation // Stateful variables, modified from the audio generation
// thread only. // thread only.
int volume_ = 0; int volume_ = 0;
int enabled_mask_ = 0; int enabled_mask_ = 0;
std::int16_t output_volume_ = 0; std::int16_t output_volume_ = 0;
std::int16_t volume_multiplier_ = 0; std::int16_t volume_multiplier_ = 0;
std::size_t subcycle_offset_ = 0; std::size_t subcycle_offset_ = 0;
void set_volume_multiplier(); void set_volume_multiplier();
}; };
} }

View File

@@ -15,27 +15,27 @@
namespace Apple::Macintosh { namespace Apple::Macintosh {
class DriveSpeedAccumulator { class DriveSpeedAccumulator {
public: public:
/*! /*!
Accepts fetched motor control values. Accepts fetched motor control values.
*/ */
void post_sample(uint8_t sample); void post_sample(uint8_t sample);
struct Delegate { struct Delegate {
virtual void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) = 0; virtual void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) = 0;
}; };
/*! /*!
Sets the delegate to receive drive speed changes. Sets the delegate to receive drive speed changes.
*/ */
void set_delegate(Delegate *delegate) { void set_delegate(Delegate *delegate) {
delegate_ = delegate; delegate_ = delegate;
} }
private: private:
static constexpr int samples_per_bucket = 20; static constexpr int samples_per_bucket = 20;
int sample_count_ = 0; int sample_count_ = 0;
int sample_total_ = 0; int sample_total_ = 0;
Delegate *delegate_ = nullptr; Delegate *delegate_ = nullptr;
}; };
} }

View File

@@ -83,206 +83,206 @@ enum class Key: uint16_t {
}; };
class Keyboard { class Keyboard {
public: public:
void set_input(bool data) { void set_input(bool data) {
switch(mode_) { switch(mode_) {
case Mode::Waiting: case Mode::Waiting:
/* /*
"Only the computer can initiate communication over the keyboard lines. When the computer and keyboard "Only the computer can initiate communication over the keyboard lines. When the computer and keyboard
are turned on, the computer is in charge of the keyboard interface and the keyboard is passive. The are turned on, the computer is in charge of the keyboard interface and the keyboard is passive. The
computer signals that it is ready to begin communication by pulling the Keyboard Data line low." computer signals that it is ready to begin communication by pulling the Keyboard Data line low."
*/ */
if(!data) { if(!data) {
mode_ = Mode::AcceptingCommand; mode_ = Mode::AcceptingCommand;
phase_ = 0; phase_ = 0;
command_ = 0; command_ = 0;
} }
break; break;
case Mode::AcceptingCommand: case Mode::AcceptingCommand:
/* Note value, so that it can be latched upon a clock transition. */ /* Note value, so that it can be latched upon a clock transition. */
data_input_ = data; data_input_ = data;
break; break;
case Mode::AwaitingEndOfCommand: case Mode::AwaitingEndOfCommand:
/* /*
The last bit of the command leaves the Keyboard Data line low; the computer then indicates that it is ready The last bit of the command leaves the Keyboard Data line low; the computer then indicates that it is ready
to receive the keyboard's response by setting the Keyboard Data line high. to receive the keyboard's response by setting the Keyboard Data line high.
*/ */
if(data) { if(data) {
mode_ = Mode::PerformingCommand; mode_ = Mode::PerformingCommand;
phase_ = 0; phase_ = 0;
} }
break; break;
default: default:
case Mode::SendingResponse: case Mode::SendingResponse:
/* This line isn't currently an input; do nothing. */ /* This line isn't currently an input; do nothing. */
break; break;
}
} }
}
bool get_clock() { bool get_clock() {
return clock_output_; return clock_output_;
}
bool get_data() {
return !!(response_ & 0x80);
}
/*!
The keyboard expects ~10 µs-frequency ticks, i.e. a clock rate of just around 100 kHz.
*/
void run_for(HalfCycles) { // TODO: honour the HalfCycles argument.
switch(mode_) {
default:
case Mode::Waiting: return;
case Mode::AcceptingCommand: {
/*
"When the computer is sending data to the keyboard, the keyboard transmits eight cycles of 400 µS each (180 µS low,
220 µS high) on the Keyboard Clock line. On the falling edge of each keyboard clock cycle, the Macintosh Plus places
a data bit on the data line and holds it there for 400 µS. The keyboard reads the data bit 80 µS after the rising edge
of the Keyboard Clock signal."
*/
const auto offset = phase_ % 40;
clock_output_ = offset >= 18;
if(offset == 26) {
command_ = (command_ << 1) | (data_input_ ? 1 : 0);
}
++phase_;
if(phase_ == 8*40) {
mode_ = Mode::AwaitingEndOfCommand;
phase_ = 0;
clock_output_ = false;
}
} break;
case Mode::AwaitingEndOfCommand:
// Time out if the end-of-command seems not to be forthcoming.
// This is an elaboration on my part; a guess.
++phase_;
if(phase_ == 1000) {
clock_output_ = false;
mode_ = Mode::Waiting;
phase_ = 0;
}
return;
case Mode::PerformingCommand: {
response_ = perform_command(command_);
// Inquiry has a 0.25-second timeout; everything else is instant.
++phase_;
if(phase_ == 25000 || command_ != 0x10 || response_ != 0x7b) {
mode_ = Mode::SendingResponse;
phase_ = 0;
}
} break;
case Mode::SendingResponse: {
/*
"When sending data to the computer, the keyboard transmits eight cycles of 330 µS each (160 µS low, 170 µS high)
on the normally high Keyboard Clock line. It places a data bit on the data line 40 µS before the falling edge of each
clock cycle and maintains it for 330 µS. The VIA in the computer latches the data bit into its shift register on the
rising edge of the Keyboard Clock signal."
*/
const auto offset = phase_ % 33;
clock_output_ = offset >= 16;
if(offset == 29) {
response_ <<= 1;
}
++phase_;
if(phase_ == 8*33) {
clock_output_ = false;
mode_ = Mode::Waiting;
phase_ = 0;
}
} break;
} }
}
bool get_data() { void enqueue_key_state(uint16_t key, bool is_pressed) {
return !!(response_ & 0x80); // Front insert; messages will be pop_back'd.
std::lock_guard lock(key_queue_mutex_);
// Keys on the keypad are preceded by a $79 keycode; in the internal naming scheme
// they are indicated by having bit 8 set. So add the $79 prefix if required.
if(key & KeypadMask) {
key_queue_.insert(key_queue_.begin(), 0x79);
} }
key_queue_.insert(key_queue_.begin(), (is_pressed ? 0x00 : 0x80) | uint8_t(key));
}
/*! private:
The keyboard expects ~10 µs-frequency ticks, i.e. a clock rate of just around 100 kHz. /// Performs the pre-ADB Apple keyboard protocol command @c command, returning
*/ /// the proper result if the command were to terminate now. So, it treats inquiry
void run_for(HalfCycles) { // TODO: honour the HalfCycles argument. /// and instant as the same command.
switch(mode_) { int perform_command(int command) {
default: switch(command) {
case Mode::Waiting: return; case 0x10: // Inquiry.
case 0x14: { // Instant.
std::lock_guard lock(key_queue_mutex_);
if(!key_queue_.empty()) {
const auto new_message = key_queue_.back();
key_queue_.pop_back();
return new_message;
}
} break;
case Mode::AcceptingCommand: { case 0x16: // Model number.
/* return
"When the computer is sending data to the keyboard, the keyboard transmits eight cycles of 400 µS each (180 µS low, 0x01 | // b0: always 1
220 µS high) on the Keyboard Clock line. On the falling edge of each keyboard clock cycle, the Macintosh Plus places (1 << 1) | // keyboard model number
a data bit on the data line and holds it there for 400 µS. The keyboard reads the data bit 80 µS after the rising edge (1 << 4); // next device number
of the Keyboard Clock signal." // (b7 not set => no next device)
*/
const auto offset = phase_ % 40;
clock_output_ = offset >= 18;
if(offset == 26) { case 0x36: // Test
command_ = (command_ << 1) | (data_input_ ? 1 : 0); return 0x7d; // 0x7d = ACK, 0x77 = not ACK.
}
++phase_;
if(phase_ == 8*40) {
mode_ = Mode::AwaitingEndOfCommand;
phase_ = 0;
clock_output_ = false;
}
} break;
case Mode::AwaitingEndOfCommand:
// Time out if the end-of-command seems not to be forthcoming.
// This is an elaboration on my part; a guess.
++phase_;
if(phase_ == 1000) {
clock_output_ = false;
mode_ = Mode::Waiting;
phase_ = 0;
}
return;
case Mode::PerformingCommand: {
response_ = perform_command(command_);
// Inquiry has a 0.25-second timeout; everything else is instant.
++phase_;
if(phase_ == 25000 || command_ != 0x10 || response_ != 0x7b) {
mode_ = Mode::SendingResponse;
phase_ = 0;
}
} break;
case Mode::SendingResponse: {
/*
"When sending data to the computer, the keyboard transmits eight cycles of 330 µS each (160 µS low, 170 µS high)
on the normally high Keyboard Clock line. It places a data bit on the data line 40 µS before the falling edge of each
clock cycle and maintains it for 330 µS. The VIA in the computer latches the data bit into its shift register on the
rising edge of the Keyboard Clock signal."
*/
const auto offset = phase_ % 33;
clock_output_ = offset >= 16;
if(offset == 29) {
response_ <<= 1;
}
++phase_;
if(phase_ == 8*33) {
clock_output_ = false;
mode_ = Mode::Waiting;
phase_ = 0;
}
} break;
}
} }
return 0x7b; // No key transition.
}
void enqueue_key_state(uint16_t key, bool is_pressed) { /// Maintains the current operating mode — a record of what the
// Front insert; messages will be pop_back'd. /// keyboard is doing now.
std::lock_guard lock(key_queue_mutex_); enum class Mode {
/// The keyboard is waiting to begin a transaction.
Waiting,
/// The keyboard is currently clocking in a new command.
AcceptingCommand,
/// The keyboard is waiting for the computer to indicate that it is ready for a response.
AwaitingEndOfCommand,
/// The keyboard is in the process of performing the command it most-recently received.
/// If the command was an 'inquiry', this state may persist for a non-neglibible period of time.
PerformingCommand,
/// The keyboard is currently shifting a response back to the computer.
SendingResponse,
} mode_ = Mode::Waiting;
// Keys on the keypad are preceded by a $79 keycode; in the internal naming scheme /// Holds a count of progress through the current @c Mode. Exact meaning depends on mode.
// they are indicated by having bit 8 set. So add the $79 prefix if required. int phase_ = 0;
if(key & KeypadMask) { /// Holds the most-recently-received command; the command is shifted into here as it is received
key_queue_.insert(key_queue_.begin(), 0x79); /// so this may not be valid prior to Mode::PerformingCommand.
} int command_ = 0;
key_queue_.insert(key_queue_.begin(), (is_pressed ? 0x00 : 0x80) | uint8_t(key)); /// Populated during PerformingCommand as the response to the most-recently-received command, this
} /// is then shifted out to the host computer. So it is guaranteed valid at the beginning of Mode::SendingResponse,
/// but not afterwards.
int response_ = 0;
private: /// The current state of the serial connection's data input.
/// Performs the pre-ADB Apple keyboard protocol command @c command, returning bool data_input_ = false;
/// the proper result if the command were to terminate now. So, it treats inquiry /// The current clock output from this keyboard.
/// and instant as the same command. bool clock_output_ = false;
int perform_command(int command) {
switch(command) {
case 0x10: // Inquiry.
case 0x14: { // Instant.
std::lock_guard lock(key_queue_mutex_);
if(!key_queue_.empty()) {
const auto new_message = key_queue_.back();
key_queue_.pop_back();
return new_message;
}
} break;
case 0x16: // Model number. /// Guards multithread access to key_queue_.
return std::mutex key_queue_mutex_;
0x01 | // b0: always 1 /// A FIFO queue for key events, in the form they'd be communicated to the Macintosh,
(1 << 1) | // keyboard model number /// with the newest events towards the front.
(1 << 4); // next device number std::vector<uint8_t> key_queue_;
// (b7 not set => no next device)
case 0x36: // Test
return 0x7d; // 0x7d = ACK, 0x77 = not ACK.
}
return 0x7b; // No key transition.
}
/// Maintains the current operating mode — a record of what the
/// keyboard is doing now.
enum class Mode {
/// The keyboard is waiting to begin a transaction.
Waiting,
/// The keyboard is currently clocking in a new command.
AcceptingCommand,
/// The keyboard is waiting for the computer to indicate that it is ready for a response.
AwaitingEndOfCommand,
/// The keyboard is in the process of performing the command it most-recently received.
/// If the command was an 'inquiry', this state may persist for a non-neglibible period of time.
PerformingCommand,
/// The keyboard is currently shifting a response back to the computer.
SendingResponse,
} mode_ = Mode::Waiting;
/// Holds a count of progress through the current @c Mode. Exact meaning depends on mode.
int phase_ = 0;
/// Holds the most-recently-received command; the command is shifted into here as it is received
/// so this may not be valid prior to Mode::PerformingCommand.
int command_ = 0;
/// Populated during PerformingCommand as the response to the most-recently-received command, this
/// is then shifted out to the host computer. So it is guaranteed valid at the beginning of Mode::SendingResponse,
/// but not afterwards.
int response_ = 0;
/// The current state of the serial connection's data input.
bool data_input_ = false;
/// The current clock output from this keyboard.
bool clock_output_ = false;
/// Guards multithread access to key_queue_.
std::mutex key_queue_mutex_;
/// A FIFO queue for key events, in the form they'd be communicated to the Macintosh,
/// with the newest events towards the front.
std::vector<uint8_t> key_queue_;
}; };
/*! /*!

File diff suppressed because it is too large Load Diff

View File

@@ -28,76 +28,76 @@ constexpr int sync_end = 38;
This class also collects audio and 400kb drive-speed data, forwarding those values. This class also collects audio and 400kb drive-speed data, forwarding those values.
*/ */
class Video { class Video {
public: public:
/*! /*!
Constructs an instance of @c Video sourcing its pixel data from @c ram and Constructs an instance of @c Video sourcing its pixel data from @c ram and
providing audio and drive-speed bytes to @c audio and @c drive_speed_accumulator. providing audio and drive-speed bytes to @c audio and @c drive_speed_accumulator.
*/ */
Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator); Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator);
/*! /*!
Sets the target device for video data. Sets the target device for video data.
*/ */
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;
/*! /*!
Produces the next @c duration period of pixels. Produces the next @c duration period of pixels.
*/ */
void run_for(HalfCycles duration); void run_for(HalfCycles duration);
/*! /*!
Sets whether the alternate screen and/or audio buffers should be used to source data. Sets whether the alternate screen and/or audio buffers should be used to source data.
*/ */
void set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer); void set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer);
/*! /*!
Provides a base address and a mask indicating which parts of the generated video and audio/drive addresses are Provides a base address and a mask indicating which parts of the generated video and audio/drive addresses are
actually decoded, accessing *word-sized memory*; e.g. for a 128kb Macintosh this should be (1 << 16) - 1 = 0xffff. actually decoded, accessing *word-sized memory*; e.g. for a 128kb Macintosh this should be (1 << 16) - 1 = 0xffff.
*/ */
void set_ram(uint16_t *ram, uint32_t mask); void set_ram(uint16_t *ram, uint32_t mask);
/*! /*!
@returns @c true if the video is currently outputting a vertical sync, @c false otherwise. @returns @c true if the video is currently outputting a vertical sync, @c false otherwise.
*/ */
bool vsync(); bool vsync();
/* /*
@returns @c true if in @c offset half cycles from now, the video will be outputting pixels; @returns @c true if in @c offset half cycles from now, the video will be outputting pixels;
@c false otherwise. @c false otherwise.
*/ */
bool is_outputting(HalfCycles offset = HalfCycles(0)) { bool is_outputting(HalfCycles offset = HalfCycles(0)) {
const auto offset_position = frame_position_ + offset % frame_length; const auto offset_position = frame_position_ + offset % frame_length;
const int column = int((offset_position % line_length).as_integral()) >> 4; const int column = int((offset_position % line_length).as_integral()) >> 4;
const int line = int((offset_position / line_length).as_integral()); const int line = int((offset_position / line_length).as_integral());
return line < 342 && column < 32; return line < 342 && column < 32;
} }
/*! /*!
@returns the amount of time until there is next a transition on the @returns the amount of time until there is next a transition on the
vsync signal. vsync signal.
*/ */
HalfCycles next_sequence_point(); HalfCycles next_sequence_point();
private: private:
DeferredAudio &audio_; DeferredAudio &audio_;
DriveSpeedAccumulator &drive_speed_accumulator_; DriveSpeedAccumulator &drive_speed_accumulator_;
Outputs::CRT::CRT crt_; Outputs::CRT::CRT crt_;
uint16_t *ram_ = nullptr; uint16_t *ram_ = nullptr;
uint32_t ram_mask_ = 0; uint32_t ram_mask_ = 0;
HalfCycles frame_position_; HalfCycles frame_position_;
size_t video_address_ = 0; size_t video_address_ = 0;
size_t audio_address_ = 0; size_t audio_address_ = 0;
uint64_t *pixel_buffer_ = nullptr; uint64_t *pixel_buffer_ = nullptr;
bool use_alternate_screen_buffer_ = false; bool use_alternate_screen_buffer_ = false;
bool use_alternate_audio_buffer_ = false; bool use_alternate_audio_buffer_ = false;
}; };
} }

View File

@@ -36,39 +36,39 @@ namespace {
namespace Atari2600 { namespace Atari2600 {
class Joystick: public Inputs::ConcreteJoystick { class Joystick: public Inputs::ConcreteJoystick {
public: public:
Joystick(Bus *bus, std::size_t shift, std::size_t fire_tia_input) : Joystick(Bus *bus, std::size_t shift, std::size_t fire_tia_input) :
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)
}), }),
bus_(bus), shift_(shift), fire_tia_input_(fire_tia_input) {} bus_(bus), shift_(shift), fire_tia_input_(fire_tia_input) {}
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) {
case Input::Up: bus_->mos6532_.update_port_input(0, 0x10 >> shift_, is_active); break; case Input::Up: bus_->mos6532_.update_port_input(0, 0x10 >> shift_, is_active); break;
case Input::Down: bus_->mos6532_.update_port_input(0, 0x20 >> shift_, is_active); break; case Input::Down: bus_->mos6532_.update_port_input(0, 0x20 >> shift_, is_active); break;
case Input::Left: bus_->mos6532_.update_port_input(0, 0x40 >> shift_, is_active); break; case Input::Left: bus_->mos6532_.update_port_input(0, 0x40 >> shift_, is_active); break;
case Input::Right: bus_->mos6532_.update_port_input(0, 0x80 >> shift_, is_active); break; case Input::Right: bus_->mos6532_.update_port_input(0, 0x80 >> shift_, is_active); break;
// TODO: latching // TODO: latching
case Input::Fire: case Input::Fire:
if(is_active) if(is_active)
bus_->tia_input_value_[fire_tia_input_] &= ~0x80; bus_->tia_input_value_[fire_tia_input_] &= ~0x80;
else else
bus_->tia_input_value_[fire_tia_input_] |= 0x80; bus_->tia_input_value_[fire_tia_input_] |= 0x80;
break; break;
default: break; default: break;
}
} }
}
private: private:
Bus *bus_; Bus *bus_;
std::size_t shift_, fire_tia_input_; std::size_t shift_, fire_tia_input_;
}; };
using Target = Analyser::Static::Atari2600::Target; using Target = Analyser::Static::Atari2600::Target;
@@ -79,133 +79,133 @@ class ConcreteMachine:
public MachineTypes::AudioProducer, public MachineTypes::AudioProducer,
public MachineTypes::ScanProducer, public MachineTypes::ScanProducer,
public MachineTypes::JoystickMachine { public MachineTypes::JoystickMachine {
public: public:
ConcreteMachine(const Target &target) : frequency_mismatch_warner_(*this) { ConcreteMachine(const Target &target) : frequency_mismatch_warner_(*this) {
const std::vector<uint8_t> &rom = target.media.cartridges.front()->get_segments().front().data; const std::vector<uint8_t> &rom = target.media.cartridges.front()->get_segments().front().data;
using PagingModel = Target::PagingModel; using PagingModel = Target::PagingModel;
switch(target.paging_model) { switch(target.paging_model) {
case PagingModel::ActivisionStack: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::ActivisionStack>>(rom); break; case PagingModel::ActivisionStack: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::ActivisionStack>>(rom); break;
case PagingModel::CBSRamPlus: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::CBSRAMPlus>>(rom); break; case PagingModel::CBSRamPlus: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::CBSRAMPlus>>(rom); break;
case PagingModel::CommaVid: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::CommaVid>>(rom); break; case PagingModel::CommaVid: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::CommaVid>>(rom); break;
case PagingModel::MegaBoy: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::MegaBoy>>(rom); break; case PagingModel::MegaBoy: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::MegaBoy>>(rom); break;
case PagingModel::MNetwork: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::MNetwork>>(rom); break; case PagingModel::MNetwork: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::MNetwork>>(rom); break;
case PagingModel::None: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Unpaged>>(rom); break; case PagingModel::None: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Unpaged>>(rom); break;
case PagingModel::ParkerBros: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::ParkerBros>>(rom); break; case PagingModel::ParkerBros: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::ParkerBros>>(rom); break;
case PagingModel::Pitfall2: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Pitfall2>>(rom); break; case PagingModel::Pitfall2: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Pitfall2>>(rom); break;
case PagingModel::Tigervision: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Tigervision>>(rom); break; case PagingModel::Tigervision: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Tigervision>>(rom); break;
case PagingModel::Atari8k: case PagingModel::Atari8k:
if(target.uses_superchip) { if(target.uses_superchip) {
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari8kSuperChip>>(rom); bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari8kSuperChip>>(rom);
} else { } else {
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari8k>>(rom); bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari8k>>(rom);
} }
break; break;
case PagingModel::Atari16k: case PagingModel::Atari16k:
if(target.uses_superchip) { if(target.uses_superchip) {
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari16kSuperChip>>(rom); bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari16kSuperChip>>(rom);
} else { } else {
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari16k>>(rom); bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari16k>>(rom);
} }
break; break;
case PagingModel::Atari32k: case PagingModel::Atari32k:
if(target.uses_superchip) { if(target.uses_superchip) {
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari32kSuperChip>>(rom); bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari32kSuperChip>>(rom);
} else { } else {
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari32k>>(rom); bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari32k>>(rom);
} }
break; break;
}
joysticks_.emplace_back(new Joystick(bus_.get(), 0, 0));
joysticks_.emplace_back(new Joystick(bus_.get(), 4, 1));
set_is_ntsc(is_ntsc_);
} }
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final { joysticks_.emplace_back(new Joystick(bus_.get(), 0, 0));
return joysticks_; joysticks_.emplace_back(new Joystick(bus_.get(), 4, 1));
set_is_ntsc(is_ntsc_);
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return joysticks_;
}
void set_switch_is_enabled(Atari2600Switch input, bool state) final {
switch(input) {
case Atari2600SwitchReset: bus_->mos6532_.update_port_input(1, 0x01, state); break;
case Atari2600SwitchSelect: bus_->mos6532_.update_port_input(1, 0x02, state); break;
case Atari2600SwitchColour: bus_->mos6532_.update_port_input(1, 0x08, state); break;
case Atari2600SwitchLeftPlayerDifficulty: bus_->mos6532_.update_port_input(1, 0x40, state); break;
case Atari2600SwitchRightPlayerDifficulty: bus_->mos6532_.update_port_input(1, 0x80, state); break;
} }
}
void set_switch_is_enabled(Atari2600Switch input, bool state) final { bool get_switch_is_enabled(Atari2600Switch input) const final {
switch(input) { uint8_t port_input = bus_->mos6532_.get_port_input(1);
case Atari2600SwitchReset: bus_->mos6532_.update_port_input(1, 0x01, state); break; switch(input) {
case Atari2600SwitchSelect: bus_->mos6532_.update_port_input(1, 0x02, state); break; case Atari2600SwitchReset: return !!(port_input & 0x01);
case Atari2600SwitchColour: bus_->mos6532_.update_port_input(1, 0x08, state); break; case Atari2600SwitchSelect: return !!(port_input & 0x02);
case Atari2600SwitchLeftPlayerDifficulty: bus_->mos6532_.update_port_input(1, 0x40, state); break; case Atari2600SwitchColour: return !!(port_input & 0x08);
case Atari2600SwitchRightPlayerDifficulty: bus_->mos6532_.update_port_input(1, 0x80, state); break; case Atari2600SwitchLeftPlayerDifficulty: return !!(port_input & 0x40);
} case Atari2600SwitchRightPlayerDifficulty: return !!(port_input & 0x80);
default: return false;
} }
}
bool get_switch_is_enabled(Atari2600Switch input) const final { void set_reset_switch(bool state) final {
uint8_t port_input = bus_->mos6532_.get_port_input(1); bus_->set_reset_line(state);
switch(input) { }
case Atari2600SwitchReset: return !!(port_input & 0x01);
case Atari2600SwitchSelect: return !!(port_input & 0x02);
case Atari2600SwitchColour: return !!(port_input & 0x08);
case Atari2600SwitchLeftPlayerDifficulty: return !!(port_input & 0x40);
case Atari2600SwitchRightPlayerDifficulty: return !!(port_input & 0x80);
default: return false;
}
}
void set_reset_switch(bool state) final { // to satisfy CRTMachine::Machine
bus_->set_reset_line(state); void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
} bus_->speaker_.set_input_rate(float(get_clock_rate() / double(CPUTicksPerAudioTick)));
bus_->tia_.set_crt_delegate(&frequency_mismatch_warner_);
bus_->tia_.set_scan_target(scan_target);
}
// to satisfy CRTMachine::Machine Outputs::Display::ScanStatus get_scaled_scan_status() const final {
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final { return bus_->tia_.get_scaled_scan_status() / 3.0f;
bus_->speaker_.set_input_rate(float(get_clock_rate() / double(CPUTicksPerAudioTick))); }
bus_->tia_.set_crt_delegate(&frequency_mismatch_warner_);
bus_->tia_.set_scan_target(scan_target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const final { Outputs::Speaker::Speaker *get_speaker() final {
return bus_->tia_.get_scaled_scan_status() / 3.0f; return &bus_->speaker_;
} }
Outputs::Speaker::Speaker *get_speaker() final { void run_for(const Cycles cycles) final {
return &bus_->speaker_; bus_->run_for(cycles);
} bus_->apply_confidence(confidence_counter_);
}
void run_for(const Cycles cycles) final { void flush_output(int) final {
bus_->run_for(cycles); bus_->flush();
bus_->apply_confidence(confidence_counter_); }
}
void flush_output(int) final { void register_crt_frequency_mismatch() {
bus_->flush(); is_ntsc_ ^= true;
} set_is_ntsc(is_ntsc_);
}
void register_crt_frequency_mismatch() { float get_confidence() final {
is_ntsc_ ^= true; return confidence_counter_.get_confidence();
set_is_ntsc(is_ntsc_); }
}
float get_confidence() final { private:
return confidence_counter_.get_confidence(); // The bus.
} std::unique_ptr<Bus> bus_;
private: // Output frame rate tracker.
// The bus. Outputs::CRT::CRTFrequencyMismatchWarner<ConcreteMachine> frequency_mismatch_warner_;
std::unique_ptr<Bus> bus_; bool is_ntsc_ = true;
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
// Output frame rate tracker. // a confidence counter
Outputs::CRT::CRTFrequencyMismatchWarner<ConcreteMachine> frequency_mismatch_warner_; Analyser::Dynamic::ConfidenceCounter confidence_counter_;
bool is_ntsc_ = true;
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
// a confidence counter void set_is_ntsc(bool is_ntsc) {
Analyser::Dynamic::ConfidenceCounter confidence_counter_; bus_->tia_.set_output_mode(is_ntsc ? TIA::OutputMode::NTSC : TIA::OutputMode::PAL);
const double clock_rate = is_ntsc ? NTSC_clock_rate : PAL_clock_rate;
void set_is_ntsc(bool is_ntsc) { bus_->speaker_.set_input_rate(float(clock_rate) / float(CPUTicksPerAudioTick));
bus_->tia_.set_output_mode(is_ntsc ? TIA::OutputMode::NTSC : TIA::OutputMode::PAL); bus_->speaker_.set_high_frequency_cutoff(float(clock_rate) / float(CPUTicksPerAudioTick * 2));
const double clock_rate = is_ntsc ? NTSC_clock_rate : PAL_clock_rate; set_clock_rate(clock_rate);
bus_->speaker_.set_input_rate(float(clock_rate) / float(CPUTicksPerAudioTick)); }
bus_->speaker_.set_high_frequency_cutoff(float(clock_rate) / float(CPUTicksPerAudioTick * 2));
set_clock_rate(clock_rate);
}
}; };
} }

File diff suppressed because it is too large Load Diff

View File

@@ -19,93 +19,93 @@
namespace Atari::ST { namespace Atari::ST {
class DMAController: public WD::WD1770::Delegate, public ClockingHint::Source, public ClockingHint::Observer { class DMAController: public WD::WD1770::Delegate, public ClockingHint::Source, public ClockingHint::Observer {
public: public:
DMAController(); DMAController();
uint16_t read(int address); uint16_t read(int address);
void write(int address, uint16_t value); void write(int address, uint16_t value);
void run_for(HalfCycles duration); void run_for(HalfCycles duration);
bool get_interrupt_line(); bool get_interrupt_line();
bool get_bus_request_line(); bool get_bus_request_line();
/*! /*!
Indicates that the DMA controller has been granted bus access to the block of memory at @c ram, which Indicates that the DMA controller has been granted bus access to the block of memory at @c ram, which
is of size @c size. is of size @c size.
@returns The number of words read or written. @returns The number of words read or written.
*/ */
int bus_grant(uint16_t *ram, size_t size); int bus_grant(uint16_t *ram, size_t size);
void set_floppy_drive_selection(bool drive1, bool drive2, bool side2); void set_floppy_drive_selection(bool drive1, bool drive2, bool side2);
void set_floppy_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive); void set_floppy_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
struct Delegate { struct Delegate {
virtual void dma_controller_did_change_output(DMAController *) = 0; virtual void dma_controller_did_change_output(DMAController *) = 0;
}; };
void set_delegate(Delegate *delegate); void set_delegate(Delegate *delegate);
void set_activity_observer(Activity::Observer *observer); void set_activity_observer(Activity::Observer *observer);
// ClockingHint::Source. // ClockingHint::Source.
ClockingHint::Preference preferred_clocking() const final; ClockingHint::Preference preferred_clocking() const final;
private: private:
HalfCycles running_time_; HalfCycles running_time_;
struct WD1772: public WD::WD1770 { struct WD1772: public WD::WD1770 {
WD1772(): WD::WD1770(WD::WD1770::P1772) { WD1772(): WD::WD1770(WD::WD1770::P1772) {
emplace_drives(2, 8000000, 300, 2); emplace_drives(2, 8000000, 300, 2);
set_is_double_density(true); // TODO: is this selectable on the ST? set_is_double_density(true); // TODO: is this selectable on the ST?
} }
void set_motor_on(bool motor_on) final { void set_motor_on(bool motor_on) final {
for_all_drives([motor_on] (Storage::Disk::Drive &drive, size_t) { for_all_drives([motor_on] (Storage::Disk::Drive &drive, size_t) {
drive.set_motor_on(motor_on); drive.set_motor_on(motor_on);
}); });
} }
void set_floppy_drive_selection(bool drive1, bool drive2, bool side2) { void set_floppy_drive_selection(bool drive1, bool drive2, bool side2) {
set_drive( set_drive(
(drive1 ? 1 : 0) | (drive1 ? 1 : 0) |
(drive2 ? 2 : 0) (drive2 ? 2 : 0)
); );
for_all_drives([side2] (Storage::Disk::Drive &drive, size_t) { for_all_drives([side2] (Storage::Disk::Drive &drive, size_t) {
drive.set_head(side2); drive.set_head(side2);
}); });
} }
void set_activity_observer(Activity::Observer *observer) { void set_activity_observer(Activity::Observer *observer) {
get_drive(0).set_activity_observer(observer, "Internal", true); get_drive(0).set_activity_observer(observer, "Internal", true);
get_drive(1).set_activity_observer(observer, "External", true); get_drive(1).set_activity_observer(observer, "External", true);
} }
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) {
get_drive(drive).set_disk(disk); get_drive(drive).set_disk(disk);
} }
} fdc_; } fdc_;
void wd1770_did_change_output(WD::WD1770 *) final; void wd1770_did_change_output(WD::WD1770 *) final;
uint16_t control_ = 0; uint16_t control_ = 0;
Delegate *delegate_ = nullptr; Delegate *delegate_ = nullptr;
bool interrupt_line_ = false; bool interrupt_line_ = false;
bool bus_request_line_ = false; bool bus_request_line_ = false;
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) final; void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) final;
// MARK: - DMA State. // MARK: - DMA State.
struct Buffer { struct Buffer {
uint8_t contents[16]; uint8_t contents[16];
bool is_full = false; bool is_full = false;
} buffer_[2]; } buffer_[2];
int active_buffer_ = 0; int active_buffer_ = 0;
int bytes_received_ = 0; int bytes_received_ = 0;
bool error_ = false; bool error_ = false;
int address_ = 0; int address_ = 0;
int byte_count_ = 0; int byte_count_ = 0;
}; };
} }

View File

@@ -54,144 +54,144 @@ class IntelligentKeyboard:
public Serial::Line<false>::ReadDelegate, public Serial::Line<false>::ReadDelegate,
public ClockingHint::Source, public ClockingHint::Source,
public Inputs::Mouse { public Inputs::Mouse {
public:
IntelligentKeyboard(Serial::Line<false> &input, Serial::Line<false> &output);
ClockingHint::Preference preferred_clocking() const final;
void run_for(HalfCycles duration);
void set_key_state(Key key, bool is_pressed);
class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const final;
};
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
return joysticks_;
}
private:
// MARK: - Key queue.
std::mutex key_queue_mutex_;
std::vector<uint8_t> key_queue_;
// MARK: - Serial line state.
int bit_count_ = 0;
int command_ = 0;
Serial::Line<false> &output_line_;
void output_bytes(std::initializer_list<uint8_t> value);
bool serial_line_did_produce_bit(Serial::Line<false> *, int bit) final;
// MARK: - Command dispatch.
std::vector<uint8_t> command_sequence_;
void dispatch_command(uint8_t command);
// MARK: - Flow control.
void reset();
void resume();
void pause();
// MARK: - Mouse.
void disable_mouse();
void set_relative_mouse_position_reporting();
void set_absolute_mouse_position_reporting(uint16_t max_x, uint16_t max_y);
void set_mouse_position(uint16_t x, uint16_t y);
void set_mouse_keycode_reporting(uint8_t delta_x, uint8_t delta_y);
void set_mouse_threshold(uint8_t x, uint8_t y);
void set_mouse_scale(uint8_t x, uint8_t y);
void set_mouse_y_downward();
void set_mouse_y_upward();
void set_mouse_button_actions(uint8_t actions);
void interrogate_mouse_position();
// Inputs::Mouse.
void move(int x, int y) final;
int get_number_of_buttons() const final;
void set_button_pressed(int index, bool is_pressed) final;
void reset_all_buttons() final;
enum class MouseMode {
Relative, Absolute, Disabled
} mouse_mode_ = MouseMode::Relative;
// Absolute positioning state.
int mouse_range_[2] = {320, 200};
int mouse_scale_[2] = {1, 1};
int mouse_position_[2] = {0, 0};
int mouse_y_multiplier_ = 1;
// Relative positioning state.
int posted_button_state_ = 0;
int mouse_threshold_[2] = {1, 1};
void post_relative_mouse_event(int x, int y);
// Received mouse state.
std::atomic<int> mouse_movement_[2]{0, 0};
std::atomic<int> mouse_button_state_{0};
std::atomic<int> mouse_button_events_{0};
// MARK: - Joystick.
void disable_joysticks();
void set_joystick_event_mode();
void set_joystick_interrogation_mode();
void set_joystick_monitoring_mode(uint8_t rate);
void set_joystick_fire_button_monitoring_mode();
struct VelocityThreshold {
uint8_t threshold;
uint8_t prior_rate;
uint8_t post_rate;
};
void set_joystick_keycode_mode(VelocityThreshold horizontal, VelocityThreshold vertical);
void interrogate_joysticks();
void clear_joystick_events();
enum class JoystickMode {
Disabled, Event, Interrogation, KeyCode
} joystick_mode_ = JoystickMode::Event;
class Joystick: public Inputs::ConcreteJoystick {
public: public:
IntelligentKeyboard(Serial::Line<false> &input, Serial::Line<false> &output); Joystick() :
ClockingHint::Preference preferred_clocking() const final; ConcreteJoystick({
void run_for(HalfCycles duration); Input(Input::Up),
Input(Input::Down),
Input(Input::Left),
Input(Input::Right),
Input(Input::Fire, 0),
}) {}
void set_key_state(Key key, bool is_pressed); void did_set_input(const Input &input, bool is_active) final {
class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper { uint8_t mask = 0;
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const final; switch(input.type) {
}; default: return;
case Input::Up: mask = 0x01; break;
case Input::Down: mask = 0x02; break;
case Input::Left: mask = 0x04; break;
case Input::Right: mask = 0x08; break;
case Input::Fire: mask = 0x80; break;
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() { if(is_active) state_ |= mask; else state_ &= ~mask;
return joysticks_; }
uint8_t get_state() {
returned_state_ = state_;
return state_;
}
bool has_event() {
return returned_state_ != state_;
}
uint8_t event_mask() {
return returned_state_ ^ state_;
} }
private: private:
// MARK: - Key queue. uint8_t state_ = 0x00;
std::mutex key_queue_mutex_; uint8_t returned_state_ = 0x00;
std::vector<uint8_t> key_queue_; };
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
// MARK: - Serial line state.
int bit_count_ = 0;
int command_ = 0;
Serial::Line<false> &output_line_;
void output_bytes(std::initializer_list<uint8_t> value);
bool serial_line_did_produce_bit(Serial::Line<false> *, int bit) final;
// MARK: - Command dispatch.
std::vector<uint8_t> command_sequence_;
void dispatch_command(uint8_t command);
// MARK: - Flow control.
void reset();
void resume();
void pause();
// MARK: - Mouse.
void disable_mouse();
void set_relative_mouse_position_reporting();
void set_absolute_mouse_position_reporting(uint16_t max_x, uint16_t max_y);
void set_mouse_position(uint16_t x, uint16_t y);
void set_mouse_keycode_reporting(uint8_t delta_x, uint8_t delta_y);
void set_mouse_threshold(uint8_t x, uint8_t y);
void set_mouse_scale(uint8_t x, uint8_t y);
void set_mouse_y_downward();
void set_mouse_y_upward();
void set_mouse_button_actions(uint8_t actions);
void interrogate_mouse_position();
// Inputs::Mouse.
void move(int x, int y) final;
int get_number_of_buttons() const final;
void set_button_pressed(int index, bool is_pressed) final;
void reset_all_buttons() final;
enum class MouseMode {
Relative, Absolute, Disabled
} mouse_mode_ = MouseMode::Relative;
// Absolute positioning state.
int mouse_range_[2] = {320, 200};
int mouse_scale_[2] = {1, 1};
int mouse_position_[2] = {0, 0};
int mouse_y_multiplier_ = 1;
// Relative positioning state.
int posted_button_state_ = 0;
int mouse_threshold_[2] = {1, 1};
void post_relative_mouse_event(int x, int y);
// Received mouse state.
std::atomic<int> mouse_movement_[2]{0, 0};
std::atomic<int> mouse_button_state_{0};
std::atomic<int> mouse_button_events_{0};
// MARK: - Joystick.
void disable_joysticks();
void set_joystick_event_mode();
void set_joystick_interrogation_mode();
void set_joystick_monitoring_mode(uint8_t rate);
void set_joystick_fire_button_monitoring_mode();
struct VelocityThreshold {
uint8_t threshold;
uint8_t prior_rate;
uint8_t post_rate;
};
void set_joystick_keycode_mode(VelocityThreshold horizontal, VelocityThreshold vertical);
void interrogate_joysticks();
void clear_joystick_events();
enum class JoystickMode {
Disabled, Event, Interrogation, KeyCode
} joystick_mode_ = JoystickMode::Event;
class Joystick: public Inputs::ConcreteJoystick {
public:
Joystick() :
ConcreteJoystick({
Input(Input::Up),
Input(Input::Down),
Input(Input::Left),
Input(Input::Right),
Input(Input::Fire, 0),
}) {}
void did_set_input(const Input &input, bool is_active) final {
uint8_t mask = 0;
switch(input.type) {
default: return;
case Input::Up: mask = 0x01; break;
case Input::Down: mask = 0x02; break;
case Input::Left: mask = 0x04; break;
case Input::Right: mask = 0x08; break;
case Input::Fire: mask = 0x80; break;
}
if(is_active) state_ |= mask; else state_ &= ~mask;
}
uint8_t get_state() {
returned_state_ = state_;
return state_;
}
bool has_event() {
return returned_state_ != state_;
}
uint8_t event_mask() {
return returned_state_ ^ state_;
}
private:
uint8_t state_ = 0x00;
uint8_t returned_state_ = 0x00;
};
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
}; };
} }

View File

@@ -31,220 +31,220 @@ struct LineLength {
(hopefully) to a subsystem. (hopefully) to a subsystem.
*/ */
class Video { class Video {
public:
Video();
/*!
Sets the memory pool that provides video, and its size in words.
*/
void set_ram(uint16_t *, size_t size);
/*!
Sets the target device for video data.
*/
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
/// Gets the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const;
/*!
Sets the type of output.
*/
void set_display_type(Outputs::Display::DisplayType);
/*!
Gets the type of output.
*/
Outputs::Display::DisplayType get_display_type() const;
/*!
Produces the next @c duration period of pixels.
*/
void run_for(HalfCycles duration);
/*!
@returns the number of cycles until there is next a change in the hsync,
vsync or display_enable outputs.
*/
HalfCycles next_sequence_point();
/*!
@returns @c true if the horizontal sync output is currently active; @c false otherwise.
@discussion On an Atari ST, this generates a VPA-style interrupt, which is often erroneously
documented as being triggered by horizontal blank.
*/
bool hsync();
/*!
@returns @c true if the vertical sync output is currently active; @c false otherwise.
@discussion On an Atari ST, this generates a VPA-style interrupt, which is often erroneously
documented as being triggered by vertical blank.
*/
bool vsync();
/*!
@returns @c true if the display enabled output is currently active; @c false otherwise.
@discussion On an Atari ST this is fed to the MFP. The documentation that I've been able to
find implies a total 28-cycle delay between the real delay enabled signal changing and its effect
on the 68000 interrupt input via the MFP. As I have yet to determine how much delay is caused
by the MFP a full 28-cycle delay is applied by this class. This should be dialled down when the
MFP's responsibility is clarified.
*/
bool display_enabled();
/// @returns the effect of reading from @c address; only the low 6 bits are decoded.
uint16_t read(int address);
/// Writes @c value to @c address, of which only the low 6 bits are decoded.
void write(int address, uint16_t value);
/// Used internally to track state.
enum class FieldFrequency {
Fifty = 0, Sixty = 1, SeventyTwo = 2
};
struct RangeObserver {
/// Indicates to the observer that the memory access range has changed.
virtual void video_did_change_access_range(Video *) = 0;
};
/// Sets a range observer, which is an actor that will be notified if the memory access range changes.
void set_range_observer(RangeObserver *);
struct Range {
uint32_t low_address, high_address;
};
/*!
@returns the range of addresses that the video might read from.
*/
Range get_memory_access_range();
private:
DeferredQueue<HalfCycles> deferrer_;
Outputs::CRT::CRT crt_;
RangeObserver *range_observer_ = nullptr;
uint16_t raw_palette_[16];
uint16_t palette_[16];
int base_address_ = 0;
int previous_base_address_ = 0;
int current_address_ = 0;
uint16_t *ram_ = nullptr;
int ram_mask_ = 0;
int x_ = 0, y_ = 0, next_y_ = 0;
bool load_ = false;
int load_base_ = 0;
uint16_t video_mode_ = 0;
uint16_t sync_mode_ = 0;
FieldFrequency field_frequency_ = FieldFrequency::Fifty;
enum class OutputBpp {
One, Two, Four
} output_bpp_ = OutputBpp::Four;
void update_output_mode();
struct HorizontalState {
bool enable = false;
bool blank = false;
bool sync = false;
} horizontal_;
struct VerticalState {
bool enable = false;
bool blank = false;
enum class SyncSchedule {
/// No sync events this line.
None,
/// Sync should begin during this horizontal line.
Begin,
/// Sync should end during this horizontal line.
End,
} sync_schedule = SyncSchedule::None;
bool sync = false;
} vertical_, next_vertical_;
LineLength line_length_;
int data_latch_position_ = 0;
int data_latch_read_position_ = 0;
uint16_t data_latch_[128];
void push_latched_data();
void reset_fifo();
/*!
Provides a target for control over the output video stream, which is considered to be
a permanently shifting shifter, that you need to reload when appropriate, which can be
overridden by the blank and sync levels.
This stream will automatically insert a colour burst.
*/
class VideoStream {
public: public:
Video(); VideoStream(Outputs::CRT::CRT &crt, uint16_t *palette) : crt_(crt), palette_(palette) {}
/*! enum class OutputMode {
Sets the memory pool that provides video, and its size in words. Sync, Blank, ColourBurst, Pixels,
*/
void set_ram(uint16_t *, size_t size);
/*!
Sets the target device for video data.
*/
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
/// Gets the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const;
/*!
Sets the type of output.
*/
void set_display_type(Outputs::Display::DisplayType);
/*!
Gets the type of output.
*/
Outputs::Display::DisplayType get_display_type() const;
/*!
Produces the next @c duration period of pixels.
*/
void run_for(HalfCycles duration);
/*!
@returns the number of cycles until there is next a change in the hsync,
vsync or display_enable outputs.
*/
HalfCycles next_sequence_point();
/*!
@returns @c true if the horizontal sync output is currently active; @c false otherwise.
@discussion On an Atari ST, this generates a VPA-style interrupt, which is often erroneously
documented as being triggered by horizontal blank.
*/
bool hsync();
/*!
@returns @c true if the vertical sync output is currently active; @c false otherwise.
@discussion On an Atari ST, this generates a VPA-style interrupt, which is often erroneously
documented as being triggered by vertical blank.
*/
bool vsync();
/*!
@returns @c true if the display enabled output is currently active; @c false otherwise.
@discussion On an Atari ST this is fed to the MFP. The documentation that I've been able to
find implies a total 28-cycle delay between the real delay enabled signal changing and its effect
on the 68000 interrupt input via the MFP. As I have yet to determine how much delay is caused
by the MFP a full 28-cycle delay is applied by this class. This should be dialled down when the
MFP's responsibility is clarified.
*/
bool display_enabled();
/// @returns the effect of reading from @c address; only the low 6 bits are decoded.
uint16_t read(int address);
/// Writes @c value to @c address, of which only the low 6 bits are decoded.
void write(int address, uint16_t value);
/// Used internally to track state.
enum class FieldFrequency {
Fifty = 0, Sixty = 1, SeventyTwo = 2
}; };
struct RangeObserver { /// Sets the current data format for the shifter. Changes in output BPP flush the shifter.
/// Indicates to the observer that the memory access range has changed. void set_bpp(OutputBpp bpp);
virtual void video_did_change_access_range(Video *) = 0;
};
/// Sets a range observer, which is an actor that will be notified if the memory access range changes. /// Outputs signal of type @c mode for @c duration.
void set_range_observer(RangeObserver *); void output(int duration, OutputMode mode);
struct Range { /// Warns the video stream that the border colour, included in the palette that it holds a pointer to,
uint32_t low_address, high_address; /// will change momentarily. This should be called after the relevant @c output() updates, and
}; /// is used to help elide border-regio output.
/*! void will_change_border_colour();
@returns the range of addresses that the video might read from.
*/ /// Loads 64 bits into the Shifter. The shifter shifts continuously. If you also declare
Range get_memory_access_range(); /// a pixels region then whatever is being shifted will reach the display, in a form that
/// depends on the current output BPP.
void load(uint64_t value);
private: private:
DeferredQueue<HalfCycles> deferrer_; // The target CRT and the palette to use.
Outputs::CRT::CRT &crt_;
uint16_t *palette_ = nullptr;
Outputs::CRT::CRT crt_; // Internal stateful processes.
RangeObserver *range_observer_ = nullptr; void generate(int duration, OutputMode mode, bool is_terminal);
uint16_t raw_palette_[16]; void flush_border();
uint16_t palette_[16]; void flush_pixels();
int base_address_ = 0; void shift(int duration);
int previous_base_address_ = 0; void output_pixels(int duration);
int current_address_ = 0;
uint16_t *ram_ = nullptr; // Internal state that is a function of output intent.
int ram_mask_ = 0; int duration_ = 0;
OutputMode output_mode_ = OutputMode::Sync;
OutputBpp bpp_ = OutputBpp::Four;
union {
uint64_t output_shifter_;
uint32_t shifter_halves_[2];
};
int x_ = 0, y_ = 0, next_y_ = 0; // Internal state for handling output serialisation.
bool load_ = false; uint16_t *pixel_buffer_ = nullptr;
int load_base_ = 0; int pixel_pointer_ = 0;
} video_stream_;
uint16_t video_mode_ = 0; /// Contains copies of the various observeable fields, after the relevant propagation delay.
uint16_t sync_mode_ = 0; struct PublicState {
bool display_enable = false;
bool hsync = false;
bool vsync = false;
} public_state_;
FieldFrequency field_frequency_ = FieldFrequency::Fifty; friend class ::VideoTester;
enum class OutputBpp {
One, Two, Four
} output_bpp_ = OutputBpp::Four;
void update_output_mode();
struct HorizontalState {
bool enable = false;
bool blank = false;
bool sync = false;
} horizontal_;
struct VerticalState {
bool enable = false;
bool blank = false;
enum class SyncSchedule {
/// No sync events this line.
None,
/// Sync should begin during this horizontal line.
Begin,
/// Sync should end during this horizontal line.
End,
} sync_schedule = SyncSchedule::None;
bool sync = false;
} vertical_, next_vertical_;
LineLength line_length_;
int data_latch_position_ = 0;
int data_latch_read_position_ = 0;
uint16_t data_latch_[128];
void push_latched_data();
void reset_fifo();
/*!
Provides a target for control over the output video stream, which is considered to be
a permanently shifting shifter, that you need to reload when appropriate, which can be
overridden by the blank and sync levels.
This stream will automatically insert a colour burst.
*/
class VideoStream {
public:
VideoStream(Outputs::CRT::CRT &crt, uint16_t *palette) : crt_(crt), palette_(palette) {}
enum class OutputMode {
Sync, Blank, ColourBurst, Pixels,
};
/// Sets the current data format for the shifter. Changes in output BPP flush the shifter.
void set_bpp(OutputBpp bpp);
/// Outputs signal of type @c mode for @c duration.
void output(int duration, OutputMode mode);
/// Warns the video stream that the border colour, included in the palette that it holds a pointer to,
/// will change momentarily. This should be called after the relevant @c output() updates, and
/// is used to help elide border-regio output.
void will_change_border_colour();
/// Loads 64 bits into the Shifter. The shifter shifts continuously. If you also declare
/// a pixels region then whatever is being shifted will reach the display, in a form that
/// depends on the current output BPP.
void load(uint64_t value);
private:
// The target CRT and the palette to use.
Outputs::CRT::CRT &crt_;
uint16_t *palette_ = nullptr;
// Internal stateful processes.
void generate(int duration, OutputMode mode, bool is_terminal);
void flush_border();
void flush_pixels();
void shift(int duration);
void output_pixels(int duration);
// Internal state that is a function of output intent.
int duration_ = 0;
OutputMode output_mode_ = OutputMode::Sync;
OutputBpp bpp_ = OutputBpp::Four;
union {
uint64_t output_shifter_;
uint32_t shifter_halves_[2];
};
// Internal state for handling output serialisation.
uint16_t *pixel_buffer_ = nullptr;
int pixel_pointer_ = 0;
} video_stream_;
/// Contains copies of the various observeable fields, after the relevant propagation delay.
struct PublicState {
bool display_enable = false;
bool hsync = false;
bool vsync = false;
} public_state_;
friend class ::VideoTester;
}; };
} }

View File

@@ -38,25 +38,25 @@ namespace Commodore::C1540 {
The attention input is also connected to CA1, similarly invertedl; the CA1 wire will be high when the bus is low and vice versa. The attention input is also connected to CA1, similarly invertedl; the CA1 wire will be high when the bus is low and vice versa.
*/ */
class SerialPortVIA: public MOS::MOS6522::IRQDelegatePortHandler { class SerialPortVIA: public MOS::MOS6522::IRQDelegatePortHandler {
public: public:
SerialPortVIA(MOS::MOS6522::MOS6522<SerialPortVIA> &via); SerialPortVIA(MOS::MOS6522::MOS6522<SerialPortVIA> &via);
uint8_t get_port_input(MOS::MOS6522::Port); uint8_t get_port_input(MOS::MOS6522::Port);
void set_port_output(MOS::MOS6522::Port, uint8_t value, uint8_t mask); void set_port_output(MOS::MOS6522::Port, uint8_t value, uint8_t mask);
void set_serial_line_state(::Commodore::Serial::Line, bool); void set_serial_line_state(::Commodore::Serial::Line, bool);
void set_serial_port(const std::shared_ptr<::Commodore::Serial::Port> &); void set_serial_port(const std::shared_ptr<::Commodore::Serial::Port> &);
private: private:
MOS::MOS6522::MOS6522<SerialPortVIA> &via_; MOS::MOS6522::MOS6522<SerialPortVIA> &via_;
uint8_t port_b_ = 0x0; uint8_t port_b_ = 0x0;
std::weak_ptr<::Commodore::Serial::Port> serial_port_; std::weak_ptr<::Commodore::Serial::Port> serial_port_;
bool attention_acknowledge_level_ = false; bool attention_acknowledge_level_ = false;
bool attention_level_input_ = true; bool attention_level_input_ = true;
bool data_level_output_ = false; bool data_level_output_ = false;
void update_data_line(); void update_data_line();
}; };
/*! /*!
@@ -76,46 +76,46 @@ class SerialPortVIA: public MOS::MOS6522::IRQDelegatePortHandler {
whether the disk head is being told to read or write, but it's unclear and I've yet to investigate. So, TODO. whether the disk head is being told to read or write, but it's unclear and I've yet to investigate. So, TODO.
*/ */
class DriveVIA: public MOS::MOS6522::IRQDelegatePortHandler { class DriveVIA: public MOS::MOS6522::IRQDelegatePortHandler {
public: public:
class Delegate { class Delegate {
public: public:
virtual void drive_via_did_step_head(void *driveVIA, int direction) = 0; virtual void drive_via_did_step_head(void *driveVIA, int direction) = 0;
virtual void drive_via_did_set_data_density(void *driveVIA, int density) = 0; virtual void drive_via_did_set_data_density(void *driveVIA, int density) = 0;
}; };
void set_delegate(Delegate *); void set_delegate(Delegate *);
uint8_t get_port_input(MOS::MOS6522::Port port); uint8_t get_port_input(MOS::MOS6522::Port port);
void set_sync_detected(bool); void set_sync_detected(bool);
void set_data_input(uint8_t); void set_data_input(uint8_t);
bool get_should_set_overflow(); bool get_should_set_overflow();
bool get_motor_enabled(); bool get_motor_enabled();
void set_control_line_output(MOS::MOS6522::Port, MOS::MOS6522::Line, bool value); void set_control_line_output(MOS::MOS6522::Port, MOS::MOS6522::Line, bool value);
void set_port_output(MOS::MOS6522::Port, uint8_t value, uint8_t direction_mask); void set_port_output(MOS::MOS6522::Port, uint8_t value, uint8_t direction_mask);
void set_activity_observer(Activity::Observer *observer); void set_activity_observer(Activity::Observer *observer);
private: private:
uint8_t port_b_ = 0xff, port_a_ = 0xff; uint8_t port_b_ = 0xff, port_a_ = 0xff;
bool should_set_overflow_ = false; bool should_set_overflow_ = false;
bool drive_motor_ = false; bool drive_motor_ = false;
uint8_t previous_port_b_output_ = 0; uint8_t previous_port_b_output_ = 0;
Delegate *delegate_ = nullptr; Delegate *delegate_ = nullptr;
Activity::Observer *observer_ = nullptr; Activity::Observer *observer_ = nullptr;
}; };
/*! /*!
An implementation of the C1540's serial port; this connects incoming line levels to the serial-port VIA. An implementation of the C1540's serial port; this connects incoming line levels to the serial-port VIA.
*/ */
class SerialPort : public ::Commodore::Serial::Port { class SerialPort : public ::Commodore::Serial::Port {
public: public:
void set_input(::Commodore::Serial::Line, ::Commodore::Serial::LineLevel); void set_input(::Commodore::Serial::Line, ::Commodore::Serial::LineLevel);
void set_serial_port_via(const std::shared_ptr<SerialPortVIA> &); void set_serial_port_via(const std::shared_ptr<SerialPortVIA> &);
private: private:
std::weak_ptr<SerialPortVIA> serial_port_VIA_; std::weak_ptr<SerialPortVIA> serial_port_VIA_;
}; };
class MachineBase: class MachineBase:
@@ -124,38 +124,38 @@ class MachineBase:
public DriveVIA::Delegate, public DriveVIA::Delegate,
public Storage::Disk::Controller { public Storage::Disk::Controller {
public: public:
MachineBase(Personality personality, const ROM::Map &roms); MachineBase(Personality personality, const ROM::Map &roms);
// to satisfy CPU::MOS6502::Processor // to satisfy CPU::MOS6502::Processor
Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value); Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value);
// to satisfy MOS::MOS6522::Delegate // to satisfy MOS::MOS6522::Delegate
virtual void mos6522_did_change_interrupt_status(void *mos6522); virtual void mos6522_did_change_interrupt_status(void *mos6522);
// to satisfy DriveVIA::Delegate // to satisfy DriveVIA::Delegate
void drive_via_did_step_head(void *driveVIA, int direction); void drive_via_did_step_head(void *driveVIA, int direction);
void drive_via_did_set_data_density(void *driveVIA, int density); void drive_via_did_set_data_density(void *driveVIA, int density);
/// Attaches the activity observer to this C1540. /// Attaches the activity observer to this C1540.
void set_activity_observer(Activity::Observer *observer); void set_activity_observer(Activity::Observer *observer);
protected: protected:
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, MachineBase, false> m6502_; CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, MachineBase, false> m6502_;
uint8_t ram_[0x800]; uint8_t ram_[0x800];
uint8_t rom_[0x4000]; uint8_t rom_[0x4000];
std::shared_ptr<SerialPortVIA> serial_port_VIA_port_handler_; std::shared_ptr<SerialPortVIA> serial_port_VIA_port_handler_;
std::shared_ptr<SerialPort> serial_port_; std::shared_ptr<SerialPort> serial_port_;
DriveVIA drive_VIA_port_handler_; DriveVIA drive_VIA_port_handler_;
MOS::MOS6522::MOS6522<DriveVIA> drive_VIA_; MOS::MOS6522::MOS6522<DriveVIA> drive_VIA_;
MOS::MOS6522::MOS6522<SerialPortVIA> serial_port_VIA_; MOS::MOS6522::MOS6522<SerialPortVIA> serial_port_VIA_;
int shift_register_ = 0, bit_window_offset_; int shift_register_ = 0, bit_window_offset_;
virtual void process_input_bit(int value); virtual void process_input_bit(int value);
virtual void process_index_hole(); virtual void process_index_hole();
}; };
} }

View File

@@ -209,17 +209,17 @@ struct Description {
/// plus all the fields provided as @c flags . /// plus all the fields provided as @c flags .
std::string description(int flags) const; std::string description(int flags) const;
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) Name name, std::string machine_name, std::string descriptive_name, FileNameT file_names, size_t size, CRC32T crc32s = uint32_t(0)
) : name{name}, machine_name{machine_name}, descriptive_name{descriptive_name}, file_names{file_names}, size{size}, crc32s{crc32s} { ) : 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.
if(this->crc32s.size() == 1 && !*this->crc32s.begin()) { if(this->crc32s.size() == 1 && !*this->crc32s.begin()) {
this->crc32s.clear(); this->crc32s.clear();
}
} }
}
}; };
/// @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
@@ -273,48 +273,48 @@ struct Request {
/// portion of a sentence, e.g. "Please supply" + request.description(0, L'*'). /// portion of a sentence, e.g. "Please supply" + request.description(0, L'*').
std::wstring description(int description_flags, wchar_t bullet_point); std::wstring description(int description_flags, wchar_t bullet_point);
private: private:
struct Node { struct Node {
enum class Type { enum class Type {
Any, All, One Any, All, One
};
Type type = Type::One;
Name name = Name::None;
/// @c true if this ROM is optional for machine startup. Generally indicates something
/// that would make emulation more accurate, but not sufficiently so to make it
/// a necessity.
bool is_optional = false;
std::vector<Node> children;
bool empty() const {
return type == Type::One && name == Name::None;
}
void add_descriptions(std::vector<Description> &) const;
bool validate(Map &) const;
void visit(
const std::function<void(ListType, size_t)> &enter_list,
const std::function<void(void)> &exit_list,
const std::function<void(ROM::Request::ListType type, const ROM::Description &, bool is_optional, size_t remaining)> &add_item
) const;
bool subtract(const ROM::Map &map);
void sort() {
// Don't do a full sort, but move anything optional to the back.
// This makes them print more nicely; it's a human-facing tweak only.
ssize_t index = ssize_t(children.size() - 1);
bool has_seen_non_optional = false;
while(index >= 0) {
has_seen_non_optional |= !children[size_t(index)].is_optional;
if(children[size_t(index)].is_optional && has_seen_non_optional) {
std::rotate(children.begin() + index, children.begin() + index + 1, children.end());
}
--index;
}
}
}; };
Node node; Type type = Type::One;
Name name = Name::None;
/// @c true if this ROM is optional for machine startup. Generally indicates something
/// that would make emulation more accurate, but not sufficiently so to make it
/// a necessity.
bool is_optional = false;
std::vector<Node> children;
Request append(Node::Type type, const Request &rhs); bool empty() const {
return type == Type::One && name == Name::None;
}
void add_descriptions(std::vector<Description> &) const;
bool validate(Map &) const;
void visit(
const std::function<void(ListType, size_t)> &enter_list,
const std::function<void(void)> &exit_list,
const std::function<void(ROM::Request::ListType type, const ROM::Description &, bool is_optional, size_t remaining)> &add_item
) const;
bool subtract(const ROM::Map &map);
void sort() {
// Don't do a full sort, but move anything optional to the back.
// This makes them print more nicely; it's a human-facing tweak only.
ssize_t index = ssize_t(children.size() - 1);
bool has_seen_non_optional = false;
while(index >= 0) {
has_seen_non_optional |= !children[size_t(index)].is_optional;
if(children[size_t(index)].is_optional && has_seen_non_optional) {
std::rotate(children.begin() + index, children.begin() + index + 1, children.end());
}
--index;
}
}
};
Node node;
Request append(Node::Type type, const Request &rhs);
}; };
} }

View File

@@ -14,15 +14,15 @@
namespace Utility { namespace Utility {
class StringSerialiser { class StringSerialiser {
public: public:
StringSerialiser(const std::string &source, bool use_linefeed_only = false); StringSerialiser(const std::string &source, bool use_linefeed_only = false);
uint8_t head(); uint8_t head();
bool advance(); bool advance();
private: private:
std::string input_string_; std::string input_string_;
std::size_t input_string_pointer_ = 0; std::size_t input_string_pointer_ = 0;
}; };
} }

View File

@@ -15,55 +15,55 @@
namespace Machine { namespace Machine {
template<typename T> class TypedDynamicMachine: public ::Machine::DynamicMachine { template<typename T> class TypedDynamicMachine: public ::Machine::DynamicMachine {
public: public:
TypedDynamicMachine(std::unique_ptr<T> &&machine) : machine_(std::move(machine)) {} TypedDynamicMachine(std::unique_ptr<T> &&machine) : machine_(std::move(machine)) {}
T *get() { return machine_.get(); } T *get() { return machine_.get(); }
TypedDynamicMachine() : TypedDynamicMachine(nullptr) {} TypedDynamicMachine() : TypedDynamicMachine(nullptr) {}
TypedDynamicMachine(TypedDynamicMachine &&rhs) : machine_(std::move(rhs.machine_)) {} TypedDynamicMachine(TypedDynamicMachine &&rhs) : machine_(std::move(rhs.machine_)) {}
TypedDynamicMachine &operator=(TypedDynamicMachine &&rhs) { TypedDynamicMachine &operator=(TypedDynamicMachine &&rhs) {
machine_ = std::move(rhs.machine_); machine_ = std::move(rhs.machine_);
return *this; return *this;
} }
#define Provide(type, name) \ #define Provide(type, name) \
type *name() final { \ type *name() final { \
return get<type>(); \ return get<type>(); \
} }
Provide(Activity::Source, activity_source) Provide(Activity::Source, activity_source)
Provide(Configurable::Device, configurable_device) Provide(Configurable::Device, configurable_device)
Provide(MachineTypes::TimedMachine, timed_machine) Provide(MachineTypes::TimedMachine, timed_machine)
Provide(MachineTypes::ScanProducer, scan_producer) Provide(MachineTypes::ScanProducer, scan_producer)
Provide(MachineTypes::AudioProducer, audio_producer) Provide(MachineTypes::AudioProducer, audio_producer)
Provide(MachineTypes::JoystickMachine, joystick_machine) Provide(MachineTypes::JoystickMachine, joystick_machine)
Provide(MachineTypes::KeyboardMachine, keyboard_machine) Provide(MachineTypes::KeyboardMachine, keyboard_machine)
Provide(MachineTypes::MouseMachine, mouse_machine) Provide(MachineTypes::MouseMachine, mouse_machine)
Provide(MachineTypes::MediaTarget, media_target) Provide(MachineTypes::MediaTarget, media_target)
#undef Provide #undef Provide
void *raw_pointer() final { void *raw_pointer() final {
return get(); return get();
} }
private: private:
template <typename Class> Class *get() { template <typename Class> Class *get() {
return dynamic_cast<Class *>(machine_.get()); return dynamic_cast<Class *>(machine_.get());
// Note to self: the below is not [currently] used // Note to self: the below is not [currently] used
// because in practice TypedDynamicMachine is instantiated // because in practice TypedDynamicMachine is instantiated
// with an abstract parent of the actual class. // with an abstract parent of the actual class.
// //
// TODO: rethink type hiding here. I think I've boxed myself // TODO: rethink type hiding here. I think I've boxed myself
// into an uncomfortable corner. // into an uncomfortable corner.
// if constexpr (std::is_base_of_v<Class, T>) { // if constexpr (std::is_base_of_v<Class, T>) {
// return static_cast<Class *>(machine_.get()); // return static_cast<Class *>(machine_.get());
// } else { // } else {
// return nullptr; // return nullptr;
// } // }
} }
std::unique_ptr<T> machine_; std::unique_ptr<T> machine_;
}; };
} }

View File

@@ -63,41 +63,41 @@ class CharacterMapper {
fresh key transition is ready to be consumed. fresh key transition is ready to be consumed.
*/ */
class Typer { class Typer {
public: public:
class Delegate: public MachineTypes::KeyActions { class Delegate: public MachineTypes::KeyActions {
public: public:
/// Informs the delegate that this typer has reached the end of its content. /// Informs the delegate that this typer has reached the end of its content.
virtual void typer_reset(Typer *typer) = 0; virtual void typer_reset(Typer *typer) = 0;
}; };
Typer(const std::string &string, HalfCycles delay, HalfCycles frequency, CharacterMapper &character_mapper, Delegate *delegate); Typer(const std::string &string, HalfCycles delay, HalfCycles frequency, CharacterMapper &character_mapper, Delegate *delegate);
/// Advances for @c duration. /// Advances for @c duration.
void run_for(const HalfCycles duration); void run_for(const HalfCycles duration);
/// Types the next character now, if there is one. /// Types the next character now, if there is one.
/// @returns @c true if there was anything left to type; @c false otherwise. /// @returns @c true if there was anything left to type; @c false otherwise.
bool type_next_character(); bool type_next_character();
/// Adds the contents of @c str to the end of the current string. /// Adds the contents of @c str to the end of the current string.
void append(const std::string &str); void append(const std::string &str);
const char BeginString = 0x02; // i.e. ASCII start of text const char BeginString = 0x02; // i.e. ASCII start of text
const char EndString = 0x03; // i.e. ASCII end of text const char EndString = 0x03; // i.e. ASCII end of text
private: private:
std::string string_; std::string string_;
std::size_t string_pointer_ = 0; std::size_t string_pointer_ = 0;
const HalfCycles frequency_; const HalfCycles frequency_;
HalfCycles counter_; HalfCycles counter_;
int phase_ = 0; int phase_ = 0;
Delegate *const delegate_; Delegate *const delegate_;
CharacterMapper &character_mapper_; CharacterMapper &character_mapper_;
uint16_t try_type_next_character(); uint16_t try_type_next_character();
const uint16_t *sequence_for_character(char) const; const uint16_t *sequence_for_character(char) const;
}; };
/*! /*!
@@ -106,47 +106,47 @@ class Typer {
*/ */
template <typename CMapper> template <typename CMapper>
class TypeRecipient: public Typer::Delegate { class TypeRecipient: public Typer::Delegate {
protected: protected:
template <typename... Args> TypeRecipient(Args&&... args) : character_mapper(std::forward<Args>(args)...) {} template <typename... Args> TypeRecipient(Args&&... args) : character_mapper(std::forward<Args>(args)...) {}
/// Attaches a typer to this class that will type @c string using @c character_mapper as a source. /// Attaches a typer to this class that will type @c string using @c character_mapper as a source.
void add_typer(const std::string &string) { void add_typer(const std::string &string) {
if(!typer_) { if(!typer_) {
typer_ = std::make_unique<Typer>(string, get_typer_delay(string), get_typer_frequency(), character_mapper, this); typer_ = std::make_unique<Typer>(string, get_typer_delay(string), get_typer_frequency(), character_mapper, this);
} else { } else {
typer_->append(string); typer_->append(string);
}
} }
}
/*! /*!
@returns @c true if the character mapper provides a mapping for @c c; @c false otherwise. @returns @c true if the character mapper provides a mapping for @c c; @c false otherwise.
*/ */
bool can_type(char c) const { bool can_type(char c) const {
const auto sequence = character_mapper.sequence_for_character(c); const auto sequence = character_mapper.sequence_for_character(c);
return sequence && sequence[0] != MachineTypes::MappedKeyboardMachine::KeyNotMapped; return sequence && sequence[0] != MachineTypes::MappedKeyboardMachine::KeyNotMapped;
} }
/*! /*!
Provided in order to conform to that part of the Typer::Delegate interface that goes above and Provided in order to conform to that part of the Typer::Delegate interface that goes above and
beyond KeyboardMachine::Machine; responds to the end of typing by clearing all keys. beyond KeyboardMachine::Machine; responds to the end of typing by clearing all keys.
*/ */
void typer_reset(Typer *) override { void typer_reset(Typer *) override {
clear_all_keys(); clear_all_keys();
// It's unsafe to deallocate typer right now, since it is the caller, but also it has a small // It's unsafe to deallocate typer right now, since it is the caller, but also it has a small
// memory footprint and it's desireable not to imply that the subclass need call it any more. // memory footprint and it's desireable not to imply that the subclass need call it any more.
// So shuffle it off into a siding. // So shuffle it off into a siding.
previous_typer_ = std::move(typer_); previous_typer_ = std::move(typer_);
typer_ = nullptr; typer_ = nullptr;
} }
virtual HalfCycles get_typer_delay(const std::string &) const { return HalfCycles(0); } virtual HalfCycles get_typer_delay(const std::string &) const { return HalfCycles(0); }
virtual HalfCycles get_typer_frequency() const { return HalfCycles(0); } virtual HalfCycles get_typer_frequency() const { return HalfCycles(0); }
std::unique_ptr<Typer> typer_; std::unique_ptr<Typer> typer_;
private: private:
std::unique_ptr<Typer> previous_typer_; std::unique_ptr<Typer> previous_typer_;
CMapper character_mapper; CMapper character_mapper;
}; };
} }

View File

@@ -26,81 +26,82 @@ constexpr uint8_t reverse_byte(uint8_t byte) {
} }
/*! Provides a class capable of generating a CRC from source data. */ /*! Provides a class capable of generating a CRC from source data. */
template <typename IntType, IntType reset_value, IntType output_xor, bool reflect_input, bool reflect_output> class Generator { template <typename IntType, IntType reset_value, IntType output_xor, bool reflect_input, bool reflect_output>
public: class Generator {
/*! public:
Instantiates a CRC16 that will compute the CRC16 specified by the supplied /*!
@c polynomial and @c reset_value. Instantiates a CRC16 that will compute the CRC16 specified by the supplied
*/ @c polynomial and @c reset_value.
constexpr Generator(IntType polynomial) noexcept: value_(reset_value) { */
const IntType top_bit = IntType(~(IntType(~0) >> 1)); constexpr Generator(IntType polynomial) noexcept: value_(reset_value) {
for(int c = 0; c < 256; c++) { const IntType top_bit = IntType(~(IntType(~0) >> 1));
IntType shift_value = IntType(c << multibyte_shift); for(int c = 0; c < 256; c++) {
for(int b = 0; b < 8; b++) { IntType shift_value = IntType(c << multibyte_shift);
IntType exclusive_or = (shift_value&top_bit) ? polynomial : 0; for(int b = 0; b < 8; b++) {
shift_value = IntType(shift_value << 1) ^ exclusive_or; IntType exclusive_or = (shift_value&top_bit) ? polynomial : 0;
} shift_value = IntType(shift_value << 1) ^ exclusive_or;
xor_table[c] = shift_value;
} }
xor_table[c] = shift_value;
} }
}
/// Resets the CRC to the reset value. /// Resets the CRC to the reset value.
void reset() { value_ = reset_value; } void reset() { value_ = reset_value; }
/// Updates the CRC to include @c byte. /// Updates the CRC to include @c byte.
void add(uint8_t byte) { void add(uint8_t byte) {
if constexpr (reflect_input) byte = reverse_byte(byte); if constexpr (reflect_input) byte = reverse_byte(byte);
value_ = IntType((value_ << 8) ^ xor_table[(value_ >> multibyte_shift) ^ byte]); value_ = IntType((value_ << 8) ^ xor_table[(value_ >> multibyte_shift) ^ byte]);
} }
/// @returns The current value of the CRC. /// @returns The current value of the CRC.
inline IntType get_value() const { inline IntType get_value() const {
IntType result = value_ ^ output_xor; IntType result = value_ ^ output_xor;
if constexpr (reflect_output) { if constexpr (reflect_output) {
IntType reflected_output = 0; IntType reflected_output = 0;
for(std::size_t c = 0; c < sizeof(IntType); ++c) { for(std::size_t c = 0; c < sizeof(IntType); ++c) {
reflected_output = IntType(reflected_output << 8) | IntType(reverse_byte(result & 0xff)); reflected_output = IntType(reflected_output << 8) | IntType(reverse_byte(result & 0xff));
result >>= 8; result >>= 8;
}
return reflected_output;
} }
return result; return reflected_output;
} }
return result;
}
/// Sets the current value of the CRC. /// Sets the current value of the CRC.
inline void set_value(IntType value) { value_ = value; } inline void set_value(IntType value) { value_ = value; }
/*! /*!
A compound for: A compound for:
reset() reset()
[add all data from @c data] [add all data from @c data]
get_value() get_value()
*/ */
template <typename Collection> IntType compute_crc(const Collection &data) { template <typename Collection> IntType compute_crc(const Collection &data) {
return compute_crc(data.begin(), data.end()); return compute_crc(data.begin(), data.end());
}
/*!
A compound for:
reset()
[add all data from @c begin to @c end]
get_value()
*/
template <typename Iterator> IntType compute_crc(Iterator begin, Iterator end) {
reset();
while(begin != end) {
add(*begin);
++begin;
} }
return get_value();
}
/*! private:
A compound for: static constexpr int multibyte_shift = (sizeof(IntType) * 8) - 8;
IntType xor_table[256];
reset() IntType value_;
[add all data from @c begin to @c end]
get_value()
*/
template <typename Iterator> IntType compute_crc(Iterator begin, Iterator end) {
reset();
while(begin != end) {
add(*begin);
++begin;
}
return get_value();
}
private:
static constexpr int multibyte_shift = (sizeof(IntType) * 8) - 8;
IntType xor_table[256];
IntType value_;
}; };
/*! /*!

View File

@@ -40,41 +40,41 @@ template <> struct LSFRPolynomial<uint8_t> {
in the specified int type. in the specified int type.
*/ */
template <typename IntType = uint64_t, IntType polynomial = LSFRPolynomial<IntType>::value> class LFSR { template <typename IntType = uint64_t, IntType polynomial = LSFRPolynomial<IntType>::value> class LFSR {
public: public:
/*! /*!
Constructs an LFSR with a random initial value. Constructs an LFSR with a random initial value.
*/ */
constexpr LFSR() noexcept { constexpr LFSR() noexcept {
// Randomise the value, ensuring it doesn't end up being 0; // Randomise the value, ensuring it doesn't end up being 0;
// don't set any top bits, in case this is a signed type. // don't set any top bits, in case this is a signed type.
while(!value_) { while(!value_) {
uint8_t *value_byte = reinterpret_cast<uint8_t *>(&value_); uint8_t *value_byte = reinterpret_cast<uint8_t *>(&value_);
for(size_t c = 0; c < sizeof(IntType); ++c) { for(size_t c = 0; c < sizeof(IntType); ++c) {
*value_byte = uint8_t(uint64_t(rand()) * 127 / RAND_MAX); *value_byte = uint8_t(uint64_t(rand()) * 127 / RAND_MAX);
++value_byte; ++value_byte;
}
} }
} }
}
/*! /*!
Constructs an LFSR with the specified initial value. Constructs an LFSR with the specified initial value.
An initial value of 0 is invalid. An initial value of 0 is invalid.
*/ */
LFSR(IntType initial_value) : value_(initial_value) {} LFSR(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,
determining the bit that was just shifted out. determining the bit that was just shifted out.
*/ */
IntType next() { IntType next() {
const auto result = IntType(value_ & 1); const auto result = IntType(value_ & 1);
value_ = IntType((value_ >> 1) ^ (result * polynomial)); value_ = IntType((value_ >> 1) ^ (result * polynomial));
return result; return result;
} }
private: private:
IntType value_ = 0; IntType value_ = 0;
}; };
template <uint64_t polynomial> class LFSRv: public LFSR<typename MinIntTypeValue<polynomial>::type, polynomial> {}; template <uint64_t polynomial> class LFSRv: public LFSR<typename MinIntTypeValue<polynomial>::type, polynomial> {};

View File

@@ -28,47 +28,47 @@ namespace Numeric {
/// = 65 /// = 65
/// ///
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, 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(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);
} }
private: 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, int value) {
if constexpr (i == index) { if constexpr (i == index) {
const int suffix = target % divider; const int suffix = target % divider;
target /= divider; target /= divider;
target -= target % size; target -= target % size;
target += value % size; target += value % size;
target *= divider; target *= divider;
target += suffix; target += suffix;
} else { } else {
NumericEncoder<Tail...>::template encode<index, i+1, divider*size>(target, value); NumericEncoder<Tail...>::template encode<index, i+1, divider*size>(target, value);
}
} }
}; }
};
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(int source) {
if constexpr (i == index) { if constexpr (i == index) {
return (source / divider) % size; return (source / divider) % size;
} else { } else {
return NumericDecoder<Tail...>::template decode<index, i+1, divider*size>(source); return NumericDecoder<Tail...>::template decode<index, i+1, divider*size>(source);
}
} }
}; }
};
}; };
} }

View File

@@ -55,297 +55,297 @@ class Delegate {
colour phase for colour composite video. colour phase for colour composite video.
*/ */
class CRT { class CRT {
private: private:
// The incoming clock lengths will be multiplied by @c time_multiplier_; this increases // The incoming clock lengths will be multiplied by @c time_multiplier_; this increases
// precision across the line. // precision across the line.
int time_multiplier_ = 1; int time_multiplier_ = 1;
// Two flywheels regulate scanning; the vertical will have a range much greater than the horizontal; // Two flywheels regulate scanning; the vertical will have a range much greater than the horizontal;
// the output divider is what that'll need to be divided by to reduce it into a 16-bit range as // the output divider is what that'll need to be divided by to reduce it into a 16-bit range as
// posted on to the scan target. // posted on to the scan target.
std::unique_ptr<Flywheel> horizontal_flywheel_, vertical_flywheel_; std::unique_ptr<Flywheel> horizontal_flywheel_, vertical_flywheel_;
int vertical_flywheel_output_divider_ = 1; int vertical_flywheel_output_divider_ = 1;
int cycles_since_horizontal_sync_ = 0; int cycles_since_horizontal_sync_ = 0;
Display::ScanTarget::Scan::EndPoint end_point(uint16_t data_offset); Display::ScanTarget::Scan::EndPoint end_point(uint16_t data_offset);
struct Scan { struct Scan {
enum Type { enum Type {
Sync, Level, Data, Blank, ColourBurst Sync, Level, Data, Blank, ColourBurst
} type = Scan::Blank; } type = Scan::Blank;
int number_of_cycles = 0, number_of_samples = 0; int number_of_cycles = 0, number_of_samples = 0;
uint8_t phase = 0, amplitude = 0; uint8_t phase = 0, amplitude = 0;
}; };
void output_scan(const Scan *scan); void output_scan(const Scan *scan);
uint8_t colour_burst_amplitude_ = 30; uint8_t colour_burst_amplitude_ = 30;
int colour_burst_phase_adjustment_ = 0xff; int colour_burst_phase_adjustment_ = 0xff;
int64_t phase_denominator_ = 1; int64_t phase_denominator_ = 1;
int64_t phase_numerator_ = 0; int64_t phase_numerator_ = 0;
int64_t colour_cycle_numerator_ = 1; int64_t colour_cycle_numerator_ = 1;
bool is_alternate_line_ = false, phase_alternates_ = false, should_be_alternate_line_ = false; bool is_alternate_line_ = false, phase_alternates_ = false, should_be_alternate_line_ = false;
void advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type, int number_of_samples); void advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type, int number_of_samples);
Flywheel::SyncEvent get_next_vertical_sync_event(bool vsync_is_requested, int cycles_to_run_for, int *cycles_advanced); Flywheel::SyncEvent get_next_vertical_sync_event(bool vsync_is_requested, int cycles_to_run_for, int *cycles_advanced);
Flywheel::SyncEvent get_next_horizontal_sync_event(bool hsync_is_requested, int cycles_to_run_for, int *cycles_advanced); Flywheel::SyncEvent get_next_horizontal_sync_event(bool hsync_is_requested, int cycles_to_run_for, int *cycles_advanced);
Delegate *delegate_ = nullptr; Delegate *delegate_ = nullptr;
int frames_since_last_delegate_call_ = 0; int frames_since_last_delegate_call_ = 0;
bool is_receiving_sync_ = false; // @c true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync); @c false otherwise. bool is_receiving_sync_ = false; // @c true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync); @c false otherwise.
bool is_accumulating_sync_ = false; // @c true if a sync level has triggered the suspicion that a vertical sync might be in progress; @c false otherwise. bool is_accumulating_sync_ = false; // @c true if a sync level has triggered the suspicion that a vertical sync might be in progress; @c false otherwise.
bool is_refusing_sync_ = false; // @c true once a vertical sync has been detected, until a prolonged period of non-sync has ended suspicion of an ongoing vertical sync. bool is_refusing_sync_ = false; // @c true once a vertical sync has been detected, until a prolonged period of non-sync has ended suspicion of an ongoing vertical sync.
int sync_capacitor_charge_threshold_ = 0; // Charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync. int sync_capacitor_charge_threshold_ = 0; // Charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync.
int cycles_of_sync_ = 0; // The number of cycles since the potential vertical sync began. int cycles_of_sync_ = 0; // The number of cycles since the potential vertical sync began.
int cycles_since_sync_ = 0; // The number of cycles since last in sync, for defeating the possibility of this being a vertical sync. int cycles_since_sync_ = 0; // The number of cycles since last in sync, for defeating the possibility of this being a vertical sync.
int cycles_per_line_ = 1; int cycles_per_line_ = 1;
Outputs::Display::ScanTarget *scan_target_ = &Outputs::Display::NullScanTarget::singleton; Outputs::Display::ScanTarget *scan_target_ = &Outputs::Display::NullScanTarget::singleton;
Outputs::Display::ScanTarget::Modals scan_target_modals_; Outputs::Display::ScanTarget::Modals scan_target_modals_;
static constexpr uint8_t DefaultAmplitude = 41; // Based upon a black level to maximum excursion and positive burst peak of: NTSC: 882 & 143; PAL: 933 & 150. static constexpr uint8_t DefaultAmplitude = 41; // Based upon a black level to maximum excursion and positive burst peak of: NTSC: 882 & 143; PAL: 933 & 150.
#ifndef NDEBUG #ifndef NDEBUG
size_t allocated_data_length_ = std::numeric_limits<size_t>::min(); size_t allocated_data_length_ = std::numeric_limits<size_t>::min();
#endif #endif
public: public:
/*! Constructs the CRT with a specified clock rate, height and colour subcarrier frequency. /*! Constructs the CRT with a specified clock rate, height and colour subcarrier frequency.
The requested number of buffers, each with the requested number of bytes per pixel, The requested number of buffers, each with the requested number of bytes per pixel,
is created for the machine to write raw pixel data to. is created for the machine to write raw pixel data to.
@param cycles_per_line The clock rate at which this CRT will be driven, specified as the number @param cycles_per_line The clock rate at which this CRT will be driven, specified as the number
of cycles expected to take up one whole scanline of the display. of cycles expected to take up one whole scanline of the display.
@param clocks_per_pixel_greatest_common_divisor The GCD of all potential lengths of a pixel @param clocks_per_pixel_greatest_common_divisor The GCD of all potential lengths of a pixel
in terms of the clock rate given as @c cycles_per_line. in terms of the clock rate given as @c cycles_per_line.
@param height_of_display The number of lines that nominally form one field of the display, rounded @param height_of_display The number of lines that nominally form one field of the display, rounded
up to the next whole integer. up to the next whole integer.
@param colour_cycle_numerator Specifies the numerator for the per-line frequency of the colour subcarrier. @param colour_cycle_numerator Specifies the numerator for the per-line frequency of the colour subcarrier.
@param colour_cycle_denominator Specifies the denominator for the per-line frequency of the colour subcarrier. @param colour_cycle_denominator Specifies the denominator for the per-line frequency of the colour subcarrier.
The colour subcarrier is taken to have colour_cycle_numerator/colour_cycle_denominator cycles per line. The colour subcarrier is taken to have colour_cycle_numerator/colour_cycle_denominator cycles per line.
@param vertical_sync_half_lines The expected length of vertical synchronisation (equalisation pulses aside), @param vertical_sync_half_lines The expected length of vertical synchronisation (equalisation pulses aside),
in multiples of half a line. in multiples of half a line.
@param data_type The format that the caller will use for input data. @param data_type The format that the caller will use for input data.
*/ */
CRT(int cycles_per_line, CRT(int cycles_per_line,
int clocks_per_pixel_greatest_common_divisor, int clocks_per_pixel_greatest_common_divisor,
int height_of_display, int height_of_display,
Outputs::Display::ColourSpace colour_space, Outputs::Display::ColourSpace colour_space,
int colour_cycle_numerator, int colour_cycle_numerator,
int colour_cycle_denominator, int colour_cycle_denominator,
int vertical_sync_half_lines, int vertical_sync_half_lines,
bool should_alternate, bool should_alternate,
Outputs::Display::InputDataType data_type); Outputs::Display::InputDataType data_type);
/*! Constructs a monitor-style CRT — one that will take only an RGB or monochrome signal, and therefore has /*! Constructs a monitor-style CRT — one that will take only an RGB or monochrome signal, and therefore has
no colour space or colour subcarrier frequency. This monitor will automatically map colour bursts to the black level. no colour space or colour subcarrier frequency. This monitor will automatically map colour bursts to the black level.
*/ */
CRT(int cycles_per_line, CRT(int cycles_per_line,
int clocks_per_pixel_greatest_common_divisor, int clocks_per_pixel_greatest_common_divisor,
int height_of_display, int height_of_display,
int vertical_sync_half_lines, int vertical_sync_half_lines,
Outputs::Display::InputDataType data_type); Outputs::Display::InputDataType data_type);
/*! Exactly identical to calling the designated constructor with colour subcarrier information /*! Exactly identical to calling the designated constructor with colour subcarrier information
looked up by display type. looked up by display type.
*/ */
CRT(int cycles_per_line, CRT(int cycles_per_line,
int minimum_cycles_per_pixel, int minimum_cycles_per_pixel,
Outputs::Display::Type display_type, Outputs::Display::Type display_type,
Outputs::Display::InputDataType data_type); Outputs::Display::InputDataType data_type);
/*! Constructs a CRT with no guaranteed expectations as to input signal other than data type; /*! Constructs a CRT with no guaranteed expectations as to input signal other than data type;
this allows for callers that intend to rely on @c set_new_timing. this allows for callers that intend to rely on @c set_new_timing.
*/ */
CRT(Outputs::Display::InputDataType data_type); CRT(Outputs::Display::InputDataType data_type);
/*! Resets the CRT with new timing information. The CRT then continues as though the new timing had /*! Resets the CRT with new timing information. The CRT then continues as though the new timing had
been provided at construction. */ been provided at construction. */
void set_new_timing( void set_new_timing(
int cycles_per_line, int cycles_per_line,
int height_of_display, int height_of_display,
Outputs::Display::ColourSpace colour_space, Outputs::Display::ColourSpace colour_space,
int colour_cycle_numerator, int colour_cycle_numerator,
int colour_cycle_denominator, int colour_cycle_denominator,
int vertical_sync_half_lines, int vertical_sync_half_lines,
bool should_alternate); bool should_alternate);
/*! Resets the CRT with new timing information derived from a new display type. The CRT then continues /*! Resets the CRT with new timing information derived from a new display type. The CRT then continues
as though the new timing had been provided at construction. */ as though the new timing had been provided at construction. */
void set_new_display_type( void set_new_display_type(
int cycles_per_line, int cycles_per_line,
Outputs::Display::Type display_type); Outputs::Display::Type display_type);
/*! Changes the type of data being supplied as input. /*! Changes the type of data being supplied as input.
*/ */
void set_new_data_type(Outputs::Display::InputDataType data_type); void set_new_data_type(Outputs::Display::InputDataType data_type);
/*! Sets the CRT's intended aspect ratio — 4.0/3.0 by default. /*! Sets the CRT's intended aspect ratio — 4.0/3.0 by default.
*/ */
void set_aspect_ratio(float aspect_ratio); void set_aspect_ratio(float aspect_ratio);
/*! Output at the sync level. /*! Output at the sync level.
@param number_of_cycles The amount of time to putput sync for. @param number_of_cycles The amount of time to putput sync for.
*/ */
void output_sync(int number_of_cycles); void output_sync(int number_of_cycles);
/*! Output at the blanking level. /*! Output at the blanking level.
@param number_of_cycles The amount of time to putput the blanking level for. @param number_of_cycles The amount of time to putput the blanking level for.
*/ */
void output_blank(int number_of_cycles); void output_blank(int number_of_cycles);
/*! Outputs the first written to the most-recently created run of data repeatedly for a prolonged period. /*! Outputs the first written to the most-recently created run of data repeatedly for a prolonged period.
@param number_of_cycles The number of cycles to repeat the output for. @param number_of_cycles The number of cycles to repeat the output for.
*/ */
void output_level(int number_of_cycles); void output_level(int number_of_cycles);
/*! Outputs @c value for @c number_of_cycles . /*! Outputs @c value for @c number_of_cycles .
@param value The value to output. @param value The value to output.
@param number_of_cycles The number of cycles to repeat the output for. @param number_of_cycles The number of cycles to repeat the output for.
*/ */
template <typename IntT> template <typename IntT>
void output_level(int number_of_cycles, IntT value) { void output_level(int number_of_cycles, IntT value) {
auto colour_pointer = reinterpret_cast<IntT *>(begin_data(1)); auto colour_pointer = reinterpret_cast<IntT *>(begin_data(1));
if(colour_pointer) *colour_pointer = value; if(colour_pointer) *colour_pointer = value;
output_level(number_of_cycles); output_level(number_of_cycles);
} }
/*! Declares that the caller has created a run of data via @c begin_data that is at least @c number_of_samples /*! Declares that the caller has created a run of data via @c begin_data that is at least @c number_of_samples
long, and that the first @c number_of_samples should be spread over @c number_of_cycles. long, and that the first @c number_of_samples should be spread over @c number_of_cycles.
@param number_of_cycles The amount of data to output. @param number_of_cycles The amount of data to output.
@param number_of_samples The number of samples of input data to output. @param number_of_samples The number of samples of input data to output.
@see @c begin_data @see @c begin_data
*/ */
void output_data(int number_of_cycles, size_t number_of_samples); void output_data(int number_of_cycles, size_t number_of_samples);
/*! A shorthand form for output_data that assumes the number of cycles to output for is the same as the number of samples. */ /*! A shorthand form for output_data that assumes the number of cycles to output for is the same as the number of samples. */
void output_data(int number_of_cycles) { void output_data(int number_of_cycles) {
output_data(number_of_cycles, size_t(number_of_cycles)); output_data(number_of_cycles, size_t(number_of_cycles));
} }
/*! Outputs a colour burst. /*! Outputs a colour burst.
@param number_of_cycles The length of the colour burst. @param number_of_cycles The length of the colour burst.
@param phase The initial phase of the colour burst in a measuring system with 256 units @param phase The initial phase of the colour burst in a measuring system with 256 units
per circle, e.g. 0 = 0 degrees, 128 = 180 degrees, 256 = 360 degree. per circle, e.g. 0 = 0 degrees, 128 = 180 degrees, 256 = 360 degree.
@param amplitude The amplitude of the colour burst in 1/255ths of the amplitude of the @param amplitude The amplitude of the colour burst in 1/255ths of the amplitude of the
positive portion of the wave. positive portion of the wave.
*/ */
void output_colour_burst(int number_of_cycles, uint8_t phase, bool is_alternate_line = false, uint8_t amplitude = DefaultAmplitude); void output_colour_burst(int number_of_cycles, uint8_t phase, bool is_alternate_line = false, uint8_t amplitude = DefaultAmplitude);
/*! Outputs a colour burst exactly in phase with CRT expectations using the idiomatic amplitude. /*! Outputs a colour burst exactly in phase with CRT expectations using the idiomatic amplitude.
@param number_of_cycles The length of the colour burst; @param number_of_cycles The length of the colour burst;
*/ */
void output_default_colour_burst(int number_of_cycles, uint8_t amplitude = DefaultAmplitude); void output_default_colour_burst(int number_of_cycles, uint8_t amplitude = DefaultAmplitude);
/*! Sets the current phase of the colour subcarrier used by output_default_colour_burst. /*! Sets the current phase of the colour subcarrier used by output_default_colour_burst.
@param phase The normalised instantaneous phase. 0.0f is the start of a colour cycle, 1.0f is the @param phase The normalised instantaneous phase. 0.0f is the start of a colour cycle, 1.0f is the
end of a colour cycle, 0.25f is a quarter of the way through a colour cycle, etc. end of a colour cycle, 0.25f is a quarter of the way through a colour cycle, etc.
*/ */
void set_immediate_default_phase(float phase); void set_immediate_default_phase(float phase);
/*! Attempts to allocate the given number of output samples for writing. /*! Attempts to allocate the given number of output samples for writing.
The beginning of the most recently allocated area is used as the start The beginning of the most recently allocated area is used as the start
of data written by a call to @c output_data; it is acceptable to write and to of data written by a call to @c output_data; it is acceptable to write and to
output less data than the amount requested but that may be less efficient. output less data than the amount requested but that may be less efficient.
Allocation should fail only if emulation is running significantly below real speed. Allocation should fail only if emulation is running significantly below real speed.
@param required_length The number of samples to allocate. @param required_length The number of samples to allocate.
@returns A pointer to the allocated area if room is available; @c nullptr otherwise. @returns A pointer to the allocated area if room is available; @c nullptr otherwise.
*/ */
inline uint8_t *begin_data(std::size_t required_length, std::size_t required_alignment = 1) { inline uint8_t *begin_data(std::size_t required_length, std::size_t required_alignment = 1) {
const auto result = scan_target_->begin_data(required_length, required_alignment); const auto result = scan_target_->begin_data(required_length, required_alignment);
#ifndef NDEBUG #ifndef NDEBUG
// If data was allocated, make a record of how much so as to be able to hold the caller to that // If data was allocated, make a record of how much so as to be able to hold the caller to that
// contract later. If allocation failed, don't constrain the caller. This allows callers that // contract later. If allocation failed, don't constrain the caller. This allows callers that
// allocate on demand but may allow one failure to hold for a longer period — e.g. until the // allocate on demand but may allow one failure to hold for a longer period — e.g. until the
// next line. // next line.
allocated_data_length_ = result ? required_length : std::numeric_limits<size_t>::max(); allocated_data_length_ = result ? required_length : std::numeric_limits<size_t>::max();
#endif #endif
return result; return result;
} }
/*! Sets the gamma exponent for the simulated screen. */ /*! Sets the gamma exponent for the simulated screen. */
void set_input_gamma(float gamma); void set_input_gamma(float gamma);
enum CompositeSourceType { enum CompositeSourceType {
/// The composite function provides continuous output. /// The composite function provides continuous output.
Continuous, Continuous,
/// The composite function provides discrete output with four unique values per colour cycle. /// The composite function provides discrete output with four unique values per colour cycle.
DiscreteFourSamplesPerCycle DiscreteFourSamplesPerCycle
}; };
/*! Provides information about the type of output the composite sampling function provides, discrete or continuous. /*! Provides information about the type of output the composite sampling function provides, discrete or continuous.
This is necessary because the CRT implementation samples discretely and therefore can use fewer intermediate This is necessary because the CRT implementation samples discretely and therefore can use fewer intermediate
samples if it can exactly duplicate the sampling rate and placement of the composite sampling function. samples if it can exactly duplicate the sampling rate and placement of the composite sampling function.
A continuous function is assumed by default. A continuous function is assumed by default.
@param type The type of output provided by the function supplied to `set_composite_sampling_function`. @param type The type of output provided by the function supplied to `set_composite_sampling_function`.
@param offset_of_first_sample The relative position within a full cycle of the colour subcarrier at which the @param offset_of_first_sample The relative position within a full cycle of the colour subcarrier at which the
first sample falls. E.g. 0.125 means "at 1/8th of the way through the complete cycle". first sample falls. E.g. 0.125 means "at 1/8th of the way through the complete cycle".
*/ */
void set_composite_function_type(CompositeSourceType type, float offset_of_first_sample = 0.0f); void set_composite_function_type(CompositeSourceType type, float offset_of_first_sample = 0.0f);
/*! Nominates a section of the display to crop to for output. */ /*! Nominates a section of the display to crop to for output. */
void set_visible_area(Outputs::Display::Rect visible_area); void set_visible_area(Outputs::Display::Rect visible_area);
/*! @returns The rectangle describing a subset of the display, allowing for sync periods. */ /*! @returns The rectangle describing a subset of the display, allowing for sync periods. */
Outputs::Display::Rect get_rect_for_area( Outputs::Display::Rect get_rect_for_area(
int first_line_after_sync, int first_line_after_sync,
int number_of_lines, int number_of_lines,
int first_cycle_after_sync, int first_cycle_after_sync,
int number_of_cycles, int number_of_cycles,
float aspect_ratio) const; float aspect_ratio) const;
/*! Sets the CRT delegate; set to @c nullptr if no delegate is desired. */ /*! Sets the CRT delegate; set to @c nullptr if no delegate is desired. */
inline void set_delegate(Delegate *delegate) { inline void set_delegate(Delegate *delegate) {
delegate_ = delegate; delegate_ = delegate;
} }
/*! Sets the scan target for CRT output. */ /*! Sets the scan target for CRT output. */
void set_scan_target(Outputs::Display::ScanTarget *); void set_scan_target(Outputs::Display::ScanTarget *);
/*! /*!
Gets current scan status, with time based fields being in the input scale — e.g. if you're supplying Gets current scan status, with time based fields being in the input scale — e.g. if you're supplying
86 cycles/line and 98 lines/field then it'll return a field duration of 86*98. 86 cycles/line and 98 lines/field then it'll return a field duration of 86*98.
*/ */
Outputs::Display::ScanStatus get_scaled_scan_status() const; Outputs::Display::ScanStatus get_scaled_scan_status() const;
/*! Sets the display type that will be nominated to the scan target. */ /*! Sets the display type that will be nominated to the scan target. */
void set_display_type(Outputs::Display::DisplayType); void set_display_type(Outputs::Display::DisplayType);
/*! Gets the last display type provided to set_display_type. */ /*! Gets the last display type provided to set_display_type. */
Outputs::Display::DisplayType get_display_type() const; Outputs::Display::DisplayType get_display_type() const;
/*! Sets the offset to apply to phase when using the PhaseLinkedLuminance8 input data type. */ /*! Sets the offset to apply to phase when using the PhaseLinkedLuminance8 input data type. */
void set_phase_linked_luminance_offset(float); void set_phase_linked_luminance_offset(float);
/*! Sets the input data type. */ /*! Sets the input data type. */
void set_input_data_type(Outputs::Display::InputDataType); void set_input_data_type(Outputs::Display::InputDataType);
/*! Sets the output brightness. */ /*! Sets the output brightness. */
void set_brightness(float); void set_brightness(float);
}; };
/*! /*!
@@ -353,44 +353,44 @@ class CRT {
ask its receiver to try a different display frequency. ask its receiver to try a different display frequency.
*/ */
template <typename Receiver> class CRTFrequencyMismatchWarner: public Outputs::CRT::Delegate { template <typename Receiver> class CRTFrequencyMismatchWarner: public Outputs::CRT::Delegate {
public: public:
CRTFrequencyMismatchWarner(Receiver &receiver) : receiver_(receiver) {} CRTFrequencyMismatchWarner(Receiver &receiver) : receiver_(receiver) {}
void crt_did_end_batch_of_frames(Outputs::CRT::CRT *, int number_of_frames, int number_of_unexpected_vertical_syncs) final { void crt_did_end_batch_of_frames(Outputs::CRT::CRT *, int number_of_frames, int number_of_unexpected_vertical_syncs) final {
frame_records_[frame_record_pointer_ % frame_records_.size()].number_of_frames = number_of_frames; frame_records_[frame_record_pointer_ % frame_records_.size()].number_of_frames = number_of_frames;
frame_records_[frame_record_pointer_ % frame_records_.size()].number_of_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs; frame_records_[frame_record_pointer_ % frame_records_.size()].number_of_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs;
++frame_record_pointer_; ++frame_record_pointer_;
if(frame_record_pointer_*2 >= frame_records_.size()*3) { if(frame_record_pointer_*2 >= frame_records_.size()*3) {
int total_number_of_frames = 0; int total_number_of_frames = 0;
int total_number_of_unexpected_vertical_syncs = 0; int total_number_of_unexpected_vertical_syncs = 0;
for(const auto &record: frame_records_) { for(const auto &record: frame_records_) {
total_number_of_frames += record.number_of_frames; total_number_of_frames += record.number_of_frames;
total_number_of_unexpected_vertical_syncs += record.number_of_unexpected_vertical_syncs; total_number_of_unexpected_vertical_syncs += record.number_of_unexpected_vertical_syncs;
} }
if(total_number_of_unexpected_vertical_syncs >= total_number_of_frames >> 1) { if(total_number_of_unexpected_vertical_syncs >= total_number_of_frames >> 1) {
reset(); reset();
receiver_.register_crt_frequency_mismatch(); receiver_.register_crt_frequency_mismatch();
}
} }
} }
}
void reset() { void reset() {
for(auto &record: frame_records_) { for(auto &record: frame_records_) {
record.number_of_frames = 0; record.number_of_frames = 0;
record.number_of_unexpected_vertical_syncs = 0; record.number_of_unexpected_vertical_syncs = 0;
}
} }
}
private: private:
Receiver &receiver_; Receiver &receiver_;
struct FrameRecord { struct FrameRecord {
int number_of_frames = 0; int number_of_frames = 0;
int number_of_unexpected_vertical_syncs = 0; int number_of_unexpected_vertical_syncs = 0;
}; };
std::array<FrameRecord, 4> frame_records_; std::array<FrameRecord, 4> frame_records_;
size_t frame_record_pointer_ = 0; size_t frame_record_pointer_ = 0;
}; };
} }

View File

@@ -229,32 +229,32 @@ struct Flywheel {
(counter_ >= expected_next_sync_ - (standard_period_ / 100)); (counter_ >= expected_next_sync_ - (standard_period_ / 100));
} }
private: private:
const int standard_period_; // The idealised length of time between syncs. const int standard_period_; // The idealised length of time between syncs.
const int retrace_time_; // A constant indicating the amount of time it takes to perform a retrace. const int retrace_time_; // A constant indicating the amount of time it takes to perform a retrace.
const int sync_error_window_; // A constant indicating the window either side of the next expected sync in which we'll accept other syncs. const int sync_error_window_; // A constant indicating the window either side of the next expected sync in which we'll accept other syncs.
int counter_ = 0; // Time since the _start_ of the last sync. int counter_ = 0; // Time since the _start_ of the last sync.
int counter_before_retrace_; // The value of _counter immediately before retrace began. int counter_before_retrace_; // The value of _counter immediately before retrace began.
int expected_next_sync_; // Our current expection of when the next sync will be encountered (which implies velocity). int expected_next_sync_; // Our current expection of when the next sync will be encountered (which implies velocity).
int number_of_surprises_ = 0; // A count of the surprising syncs. int number_of_surprises_ = 0; // A count of the surprising syncs.
int number_of_retraces_ = 0; // A count of the number of retraces to date. int number_of_retraces_ = 0; // A count of the number of retraces to date.
int last_adjustment_ = 0; // The amount by which expected_next_sync_ was adjusted at the last sync. int last_adjustment_ = 0; // The amount by which expected_next_sync_ was adjusted at the last sync.
/* /*
Implementation notes: Implementation notes:
Retrace takes a fixed amount of time and runs during [0, _retrace_time). Retrace takes a fixed amount of time and runs during [0, _retrace_time).
For the current line, scan then occurs from [_retrace_time, _expected_next_sync), at which point For the current line, scan then occurs from [_retrace_time, _expected_next_sync), at which point
retrace begins and the internal counter is reset. retrace begins and the internal counter is reset.
All synchronisation events that occur within (-_sync_error_window, _sync_error_window) of the All synchronisation events that occur within (-_sync_error_window, _sync_error_window) of the
expected synchronisation time will cause a proportional adjustment in the expected time for the next expected synchronisation time will cause a proportional adjustment in the expected time for the next
synchronisation. Other synchronisation events are clamped as though they occurred in that range. synchronisation. Other synchronisation events are clamped as though they occurred in that range.
*/ */
}; };
} }

View File

@@ -22,36 +22,36 @@ namespace Outputs::Display {
to allow for host-client frame synchronisation. to allow for host-client frame synchronisation.
*/ */
class Metrics { class Metrics {
public: public:
/// Notifies Metrics of a beam event. /// Notifies Metrics of a beam event.
void announce_event(ScanTarget::Event event); void announce_event(ScanTarget::Event);
/// Notifies Metrics that the size of the output buffer has changed. /// Notifies Metrics that the size of the output buffer has changed.
void announce_did_resize(); void announce_did_resize();
/// Provides Metrics with a new data point for output speed estimation. /// Provides Metrics with a new data point for output speed estimation.
void announce_draw_status(size_t lines, std::chrono::high_resolution_clock::duration duration, bool complete); void announce_draw_status(size_t lines, std::chrono::high_resolution_clock::duration, bool complete);
/// Provides Metrics with a new data point for output speed estimation, albeit without line-specific information. /// Provides Metrics with a new data point for output speed estimation, albeit without line-specific information.
void announce_draw_status(bool complete); void announce_draw_status(bool complete);
/// @returns @c true if Metrics thinks a lower output buffer resolution is desirable in the abstract; @c false otherwise. /// @returns @c true if Metrics thinks a lower output buffer resolution is desirable in the abstract; @c false otherwise.
bool should_lower_resolution() const; bool should_lower_resolution() const;
/// @returns An estimate of the number of lines being produced per frame, excluding vertical sync. /// @returns An estimate of the number of lines being produced per frame, excluding vertical sync.
float visible_lines_per_frame_estimate() const; float visible_lines_per_frame_estimate() const;
/// @returns The number of lines since vertical retrace ended. /// @returns The number of lines since vertical retrace ended.
int current_line() const; int current_line() const;
private: private:
int lines_this_frame_ = 0; int lines_this_frame_ = 0;
std::array<int, 20> line_total_history_; std::array<int, 20> line_total_history_;
size_t line_total_history_pointer_ = 0; size_t line_total_history_pointer_ = 0;
void add_line_total(int); void add_line_total(int);
std::atomic<int> frames_hit_ = 0; std::atomic<int> frames_hit_ = 0;
std::atomic<int> frames_missed_ = 0; std::atomic<int> frames_missed_ = 0;
}; };
} }

View File

@@ -137,31 +137,31 @@ public:
static constexpr bool enabled = is_enabled(source); static constexpr bool enabled = is_enabled(source);
struct LogLine { struct LogLine {
public: public:
LogLine(FILE *const stream) : stream_(stream) { LogLine(FILE *const stream) : stream_(stream) {
if constexpr (!enabled) return; if constexpr (!enabled) return;
const auto source_prefix = prefix(source); const auto source_prefix = prefix(source);
if(source_prefix) { if(source_prefix) {
fprintf(stream_, "[%s] ", source_prefix); fprintf(stream_, "[%s] ", source_prefix);
}
} }
}
~LogLine() { ~LogLine() {
if constexpr (!enabled) return; if constexpr (!enabled) return;
fprintf(stream_, "\n"); fprintf(stream_, "\n");
} }
void append(const char *const format, ...) { void append(const char *const format, ...) {
if constexpr (!enabled) return; if constexpr (!enabled) return;
va_list args; va_list args;
va_start(args, format); va_start(args, format);
vfprintf(stream_, format, args); vfprintf(stream_, format, args);
va_end(args); va_end(args);
} }
private: private:
FILE *stream_; FILE *stream_;
}; };
LogLine info() { return LogLine(stdout); } LogLine info() { return LogLine(stdout); }

View File

@@ -18,21 +18,21 @@ namespace Outputs::Display::OpenGL {
Provides a wrapper for drawing a solid, single-colour rectangle. Provides a wrapper for drawing a solid, single-colour rectangle.
*/ */
class Rectangle { class Rectangle {
public: public:
/*! /*!
Instantiates an instance of Rectange with the coordinates given. Instantiates an instance of Rectange with the coordinates given.
*/ */
Rectangle(float x, float y, float width, float height); Rectangle(float x, float y, float width, float height);
/*! /*!
Draws this rectangle in the colour supplied. Draws this rectangle in the colour supplied.
*/ */
void draw(float red, float green, float blue); void draw(float red, float green, float blue);
private: private:
Shader pixel_shader_; Shader pixel_shader_;
GLuint drawing_vertex_array_ = 0, drawing_array_buffer_ = 0; GLuint drawing_vertex_array_ = 0, drawing_array_buffer_ = 0;
GLint colour_uniform_; GLint colour_uniform_;
}; };
} }

View File

@@ -19,70 +19,70 @@ namespace Outputs::Display::OpenGL {
handles render-to-texture framebuffer objects. handles render-to-texture framebuffer objects.
*/ */
class TextureTarget { class TextureTarget {
public: public:
/*! /*!
Creates a new texture target. Contents are initially undefined. Creates a new texture target. Contents are initially undefined.
Leaves both the generated texture and framebuffer bound. Leaves both the generated texture and framebuffer bound.
@throws std::runtime_error if creation fails. @throws std::runtime_error if creation fails.
@param width The width of target to create. @param width The width of target to create.
@param height The height of target to create. @param height The height of target to create.
@param texture_unit A texture unit on which to bind the texture. @param texture_unit A texture unit on which to bind the texture.
@param has_stencil_buffer An 8-bit stencil buffer is attached if this is @c true; no stencil buffer is attached otherwise. @param has_stencil_buffer An 8-bit stencil buffer is attached if this is @c true; no stencil buffer is attached otherwise.
*/ */
TextureTarget(GLsizei width, GLsizei height, GLenum texture_unit, GLint mag_filter, bool has_stencil_buffer); TextureTarget(GLsizei width, GLsizei height, GLenum texture_unit, GLint mag_filter, bool has_stencil_buffer);
~TextureTarget(); ~TextureTarget();
/*! /*!
Binds this target as a framebuffer and sets the @c glViewport accordingly. Binds this target as a framebuffer and sets the @c glViewport accordingly.
*/ */
void bind_framebuffer(); void bind_framebuffer();
/*! /*!
Binds this target as a texture. Binds this target as a texture.
*/ */
void bind_texture() const; void bind_texture() const;
/*! /*!
@returns the width of the texture target. @returns the width of the texture target.
*/ */
GLsizei get_width() const { GLsizei get_width() const {
return width_; return width_;
} }
/*! /*!
@returns the height of the texture target. @returns the height of the texture target.
*/ */
GLsizei get_height() const { GLsizei get_height() const {
return height_; return height_;
} }
/*! /*!
Draws this texture to the currently-bound framebuffer, which has the aspect ratio Draws this texture to the currently-bound framebuffer, which has the aspect ratio
@c aspect_ratio. This texture will fill the height of the frame buffer, and pick @c aspect_ratio. This texture will fill the height of the frame buffer, and pick
an appropriate width based on the aspect ratio. an appropriate width based on the aspect ratio.
@c colour_threshold sets a threshold test that each colour must satisfy to be @c colour_threshold sets a threshold test that each colour must satisfy to be
output. A threshold of 0.0f means that all colours will pass through. A threshold output. A threshold of 0.0f means that all colours will pass through. A threshold
of 0.5f means that only colour components above 0.5f will pass through, with of 0.5f means that only colour components above 0.5f will pass through, with
0.5f being substituted elsewhere. This provides a way to ensure that the sort of 0.5f being substituted elsewhere. This provides a way to ensure that the sort of
persistent low-value errors that can result from an IIR are hidden. persistent low-value errors that can result from an IIR are hidden.
*/ */
void draw(float aspect_ratio, float colour_threshold = 0.0f) const; void draw(float aspect_ratio, float colour_threshold = 0.0f) const;
private: private:
GLuint framebuffer_ = 0, texture_ = 0, renderbuffer_ = 0; GLuint framebuffer_ = 0, texture_ = 0, renderbuffer_ = 0;
const GLsizei width_ = 0, height_ = 0; const GLsizei width_ = 0, height_ = 0;
GLsizei expanded_width_ = 0, expanded_height_ = 0; GLsizei expanded_width_ = 0, expanded_height_ = 0;
const GLenum texture_unit_ = 0; const GLenum texture_unit_ = 0;
mutable std::unique_ptr<Shader> pixel_shader_; mutable std::unique_ptr<Shader> pixel_shader_;
mutable GLuint drawing_vertex_array_ = 0, drawing_array_buffer_ = 0; mutable GLuint drawing_vertex_array_ = 0, drawing_array_buffer_ = 0;
mutable float set_aspect_ratio_ = 0.0f; mutable float set_aspect_ratio_ = 0.0f;
mutable GLint threshold_uniform_; mutable GLint threshold_uniform_;
}; };
} }

View File

@@ -36,124 +36,124 @@ namespace Outputs::Display::OpenGL {
drawn to the target framebuffer is a quad. drawn to the target framebuffer is a quad.
*/ */
class ScanTarget: public Outputs::Display::BufferingScanTarget { // TODO: use private inheritance and expose only display_metrics() and a custom cast? class ScanTarget: public Outputs::Display::BufferingScanTarget { // TODO: use private inheritance and expose only display_metrics() and a custom cast?
public: public:
ScanTarget(GLuint target_framebuffer = 0, float output_gamma = 2.2f); ScanTarget(GLuint target_framebuffer = 0, float output_gamma = 2.2f);
~ScanTarget(); ~ScanTarget();
void set_target_framebuffer(GLuint); void set_target_framebuffer(GLuint);
/*! Pushes the current state of output to the target framebuffer. */ /*! Pushes the current state of output to the target framebuffer. */
void draw(int output_width, int output_height); void draw(int output_width, int output_height);
/*! Processes all the latest input, at a resolution suitable for later output to a framebuffer of the specified size. */ /*! Processes all the latest input, at a resolution suitable for later output to a framebuffer of the specified size. */
void update(int output_width, int output_height); void update(int output_width, int output_height);
private: private:
static constexpr int LineBufferWidth = 2048; static constexpr int LineBufferWidth = 2048;
static constexpr int LineBufferHeight = 2048; static constexpr int LineBufferHeight = 2048;
#ifndef NDEBUG #ifndef NDEBUG
struct OpenGLVersionDumper { struct OpenGLVersionDumper {
OpenGLVersionDumper() { OpenGLVersionDumper() {
// Note the OpenGL version, as the first thing this class does prior to construction. // Note the OpenGL version, as the first thing this class does prior to construction.
Log::Logger<Log::Source::OpenGL>().info().append( Log::Logger<Log::Source::OpenGL>().info().append(
"Constructing scan target with OpenGL %s; shading language version %s", "Constructing scan target with OpenGL %s; shading language version %s",
glGetString(GL_VERSION), glGetString(GL_VERSION),
glGetString(GL_SHADING_LANGUAGE_VERSION)); glGetString(GL_SHADING_LANGUAGE_VERSION));
} }
} dumper_; } dumper_;
#endif #endif
GLuint target_framebuffer_; GLuint target_framebuffer_;
const float output_gamma_; const float output_gamma_;
int resolution_reduction_level_ = 1; int resolution_reduction_level_ = 1;
int output_height_ = 0; int output_height_ = 0;
size_t lines_submitted_ = 0; size_t lines_submitted_ = 0;
std::chrono::high_resolution_clock::time_point line_submission_begin_time_; std::chrono::high_resolution_clock::time_point line_submission_begin_time_;
// Contains the first composition of scans into lines; // Contains the first composition of scans into lines;
// they're accumulated prior to output to allow for continuous // they're accumulated prior to output to allow for continuous
// application of any necessary conversions — e.g. composite processing. // application of any necessary conversions — e.g. composite processing.
TextureTarget unprocessed_line_texture_; TextureTarget unprocessed_line_texture_;
// Contains pre-lowpass-filtered chrominance information that is // Contains pre-lowpass-filtered chrominance information that is
// part-QAM-demoduled, if dealing with a QAM data source. // part-QAM-demoduled, if dealing with a QAM data source.
std::unique_ptr<TextureTarget> qam_chroma_texture_; std::unique_ptr<TextureTarget> qam_chroma_texture_;
// Scans are accumulated to the accumulation texture; the full-display // Scans are accumulated to the accumulation texture; the full-display
// rectangle is used to ensure untouched pixels properly decay. // rectangle is used to ensure untouched pixels properly decay.
std::unique_ptr<TextureTarget> accumulation_texture_; std::unique_ptr<TextureTarget> accumulation_texture_;
Rectangle full_display_rectangle_; Rectangle full_display_rectangle_;
bool stencil_is_valid_ = false; bool stencil_is_valid_ = false;
// OpenGL storage handles for buffer data. // OpenGL storage handles for buffer data.
GLuint scan_buffer_name_ = 0, scan_vertex_array_ = 0; GLuint scan_buffer_name_ = 0, scan_vertex_array_ = 0;
GLuint line_buffer_name_ = 0, line_vertex_array_ = 0; GLuint line_buffer_name_ = 0, line_vertex_array_ = 0;
template <typename T> void allocate_buffer(const T &array, GLuint &buffer_name, GLuint &vertex_array_name); template <typename T> void allocate_buffer(const T &array, GLuint &buffer_name, GLuint &vertex_array_name);
template <typename T> void patch_buffer(const T &array, GLuint target, uint16_t submit_pointer, uint16_t read_pointer); template <typename T> void patch_buffer(const T &array, GLuint target, uint16_t submit_pointer, uint16_t read_pointer);
GLuint write_area_texture_name_ = 0; GLuint write_area_texture_name_ = 0;
bool texture_exists_ = false; bool texture_exists_ = false;
// Receives scan target modals. // Receives scan target modals.
void setup_pipeline(); void setup_pipeline();
enum class ShaderType { enum class ShaderType {
Composition, Composition,
Conversion, Conversion,
QAMSeparation QAMSeparation
}; };
/*! /*!
Calls @c taret.enable_vertex_attribute_with_pointer to attach all Calls @c taret.enable_vertex_attribute_with_pointer to attach all
globals for shaders of @c type to @c target. globals for shaders of @c type to @c target.
*/ */
static void enable_vertex_attributes(ShaderType type, Shader &target); static void enable_vertex_attributes(ShaderType type, Shader &target);
void set_uniforms(ShaderType type, Shader &target) const; void set_uniforms(ShaderType type, Shader &target) const;
std::vector<std::string> bindings(ShaderType type) const; std::vector<std::string> bindings(ShaderType type) const;
GLsync fence_ = nullptr; GLsync fence_ = nullptr;
std::atomic_flag is_drawing_to_accumulation_buffer_; std::atomic_flag is_drawing_to_accumulation_buffer_;
std::unique_ptr<Shader> input_shader_; std::unique_ptr<Shader> input_shader_;
std::unique_ptr<Shader> output_shader_; std::unique_ptr<Shader> output_shader_;
std::unique_ptr<Shader> qam_separation_shader_; std::unique_ptr<Shader> qam_separation_shader_;
/*! /*!
Produces a shader that composes fragment of the input stream to a single buffer, Produces a shader that composes fragment of the input stream to a single buffer,
normalising the data into one of four forms: RGB, 8-bit luminance, normalising the data into one of four forms: RGB, 8-bit luminance,
phase-linked luminance or luminance+phase offset. phase-linked luminance or luminance+phase offset.
*/ */
std::unique_ptr<Shader> composition_shader() const; std::unique_ptr<Shader> composition_shader() const;
/*! /*!
Produces a shader that reads from a composition buffer and converts to host Produces a shader that reads from a composition buffer and converts to host
output RGB, decoding composite or S-Video as necessary. output RGB, decoding composite or S-Video as necessary.
*/ */
std::unique_ptr<Shader> conversion_shader() const; std::unique_ptr<Shader> conversion_shader() const;
/*! /*!
Produces a shader that writes separated but not-yet filtered QAM components Produces a shader that writes separated but not-yet filtered QAM components
from the unprocessed line texture to the QAM chroma texture, at a fixed from the unprocessed line texture to the QAM chroma texture, at a fixed
size of four samples per colour clock, point sampled. size of four samples per colour clock, point sampled.
*/ */
std::unique_ptr<Shader> qam_separation_shader() const; std::unique_ptr<Shader> qam_separation_shader() const;
void set_sampling_window(int output_Width, int output_height, Shader &target); void set_sampling_window(int output_Width, int output_height, Shader &target);
std::string sampling_function() const; std::string sampling_function() const;
/*! /*!
@returns true if the current display type is a 'soft' one, i.e. one in which @returns true if the current display type is a 'soft' one, i.e. one in which
contrast tends to be low, such as a composite colour display. contrast tends to be low, such as a composite colour display.
*/ */
bool is_soft_display_type(); bool is_soft_display_type();
// Storage for the various buffers. // Storage for the various buffers.
std::vector<uint8_t> write_area_texture_; std::vector<uint8_t> write_area_texture_;
std::array<Scan, LineBufferHeight*5> scan_buffer_; std::array<Scan, LineBufferHeight*5> scan_buffer_;
std::array<Line, LineBufferHeight> line_buffer_; std::array<Line, LineBufferHeight> line_buffer_;
std::array<LineMetadata, LineBufferHeight> line_metadata_buffer_; std::array<LineMetadata, LineBufferHeight> line_metadata_buffer_;
}; };
} }

View File

@@ -33,241 +33,241 @@ namespace Outputs::Display {
This buffer rejects new data when full. This buffer rejects new data when full.
*/ */
class BufferingScanTarget: public Outputs::Display::ScanTarget { class BufferingScanTarget: public Outputs::Display::ScanTarget {
public: public:
/*! @returns The DisplayMetrics object that this ScanTarget has been providing with announcements and draw overages. */ /*! @returns The DisplayMetrics object that this ScanTarget has been providing with announcements and draw overages. */
const Metrics &display_metrics(); const Metrics &display_metrics();
static constexpr int WriteAreaWidth = 2048; static constexpr int WriteAreaWidth = 2048;
static constexpr int WriteAreaHeight = 2048; static constexpr int WriteAreaHeight = 2048;
BufferingScanTarget(); BufferingScanTarget();
// This is included because it's assumed that scan targets will want to expose one. // This is included because it's assumed that scan targets will want to expose one.
// It is the subclass's responsibility to post timings. // It is the subclass's responsibility to post timings.
Metrics display_metrics_; Metrics display_metrics_;
/// Extends the definition of a Scan to include two extra fields, /// Extends the definition of a Scan to include two extra fields,
/// completing this scan's source data and destination locations. /// completing this scan's source data and destination locations.
struct Scan { struct Scan {
Outputs::Display::ScanTarget::Scan scan; Outputs::Display::ScanTarget::Scan scan;
/// Stores the y coordinate for this scan's data within the write area texture. /// Stores the y coordinate for this scan's data within the write area texture.
/// Use this plus the scan's endpoints' data_offsets to locate this data in 2d. /// Use this plus the scan's endpoints' data_offsets to locate this data in 2d.
/// Note that the data_offsets will have been adjusted to be relative to the line /// Note that the data_offsets will have been adjusted to be relative to the line
/// they fall within, not the data allocation. /// they fall within, not the data allocation.
uint16_t data_y; uint16_t data_y;
/// Stores the y coordinate assigned to this scan within the intermediate buffers. /// Stores the y coordinate assigned to this scan within the intermediate buffers.
/// Use this plus this scan's endpoints' x locations to determine where to composite /// Use this plus this scan's endpoints' x locations to determine where to composite
/// this data for intermediate processing. /// this data for intermediate processing.
uint16_t line; uint16_t line;
};
/// Defines the boundaries of a complete line of video — a 2d start and end location,
/// composite phase and amplitude (if relevant), the source line in the intermediate buffer
/// plus the start and end offsets of the area that is visible from the intermediate buffer.
struct Line {
struct EndPoint {
uint16_t x, y;
int16_t composite_angle;
uint16_t cycles_since_end_of_horizontal_retrace;
} end_points[2];
uint8_t composite_amplitude;
uint16_t line;
};
/// Provides additional metadata about lines; this is separate because it's unlikely to be of
/// interest to the GPU, unlike the fields in Line.
struct LineMetadata {
/// @c true if this line was the first drawn after vertical sync; @c false otherwise.
bool is_first_in_frame;
/// @c true if this line is the first in the frame and if every single piece of output
/// from the previous frame was recorded; @c false otherwise. Data can be dropped
/// from a frame if performance problems mean that the emulated machine is running
/// more quickly than complete frames can be generated.
bool previous_frame_was_complete;
/// The index of the first scan that will appear on this line.
size_t first_scan;
};
/// Sets the area of memory to use as a scan buffer.
void set_scan_buffer(Scan *buffer, size_t size);
/// Sets the area of memory to use as line and line metadata buffers.
void set_line_buffer(Line *line_buffer, LineMetadata *metadata_buffer, size_t size);
/// Sets a new base address for the texture.
/// When called this will flush all existing data and load up the
/// new data size.
void set_write_area(uint8_t *base);
/// @returns The number of bytes per input sample, as per the latest modals.
size_t write_area_data_size() const;
/// Defines a segment of data now ready for output, consisting of start and endpoints for:
///
/// (i) the region of the write area that has been modified; if the caller is using shared memory
/// for the write area then it can ignore this information;
///
/// (ii) the number of scans that have been completed; and
///
/// (iii) the number of lines that have been completed.
///
/// New write areas and scans are exposed only upon completion of the corresponding lines.
/// The values indicated by the start point are the first that should be drawn. Those indicated
/// by the end point are one after the final that should be drawn.
///
/// So e.g. start.scan = 23, end.scan = 24 means draw a single scan, index 23.
struct OutputArea {
struct Endpoint {
int write_area_x, write_area_y;
size_t scan;
size_t line;
}; };
/// Defines the boundaries of a complete line of video — a 2d start and end location, Endpoint start, end;
/// composite phase and amplitude (if relevant), the source line in the intermediate buffer
/// plus the start and end offsets of the area that is visible from the intermediate buffer.
struct Line {
struct EndPoint {
uint16_t x, y;
int16_t composite_angle;
uint16_t cycles_since_end_of_horizontal_retrace;
} end_points[2];
uint8_t composite_amplitude;
uint16_t line;
};
/// Provides additional metadata about lines; this is separate because it's unlikely to be of
/// interest to the GPU, unlike the fields in Line.
struct LineMetadata {
/// @c true if this line was the first drawn after vertical sync; @c false otherwise.
bool is_first_in_frame;
/// @c true if this line is the first in the frame and if every single piece of output
/// from the previous frame was recorded; @c false otherwise. Data can be dropped
/// from a frame if performance problems mean that the emulated machine is running
/// more quickly than complete frames can be generated.
bool previous_frame_was_complete;
/// The index of the first scan that will appear on this line.
size_t first_scan;
};
/// Sets the area of memory to use as a scan buffer.
void set_scan_buffer(Scan *buffer, size_t size);
/// Sets the area of memory to use as line and line metadata buffers.
void set_line_buffer(Line *line_buffer, LineMetadata *metadata_buffer, size_t size);
/// Sets a new base address for the texture.
/// When called this will flush all existing data and load up the
/// new data size.
void set_write_area(uint8_t *base);
/// @returns The number of bytes per input sample, as per the latest modals.
size_t write_area_data_size() const;
/// Defines a segment of data now ready for output, consisting of start and endpoints for:
///
/// (i) the region of the write area that has been modified; if the caller is using shared memory
/// for the write area then it can ignore this information;
///
/// (ii) the number of scans that have been completed; and
///
/// (iii) the number of lines that have been completed.
///
/// New write areas and scans are exposed only upon completion of the corresponding lines.
/// The values indicated by the start point are the first that should be drawn. Those indicated
/// by the end point are one after the final that should be drawn.
///
/// So e.g. start.scan = 23, end.scan = 24 means draw a single scan, index 23.
struct OutputArea {
struct Endpoint {
int write_area_x, write_area_y;
size_t scan;
size_t line;
};
Endpoint start, end;
#ifndef NDEBUG #ifndef NDEBUG
size_t counter; size_t counter;
#endif #endif
}; };
/// Gets the current range of content that has been posted but not yet returned by /// Gets the current range of content that has been posted but not yet returned by
/// a previous call to get_output_area(). /// a previous call to get_output_area().
/// ///
/// Does not require the caller to be within a @c perform block. /// Does not require the caller to be within a @c perform block.
OutputArea get_output_area(); OutputArea get_output_area();
/// Announces that the output area has now completed output, freeing up its memory for /// Announces that the output area has now completed output, freeing up its memory for
/// further modification. /// further modification.
/// ///
/// It is the caller's responsibility to ensure that the areas passed to complete_output_area /// It is the caller's responsibility to ensure that the areas passed to complete_output_area
/// are those from get_output_area and are marked as completed in the same order that /// are those from get_output_area and are marked as completed in the same order that
/// they were originally provided. /// they were originally provided.
/// ///
/// Does not require the caller to be within a @c perform block. /// Does not require the caller to be within a @c perform block.
void complete_output_area(const OutputArea &); void complete_output_area(const OutputArea &);
/// Performs @c action ensuring that no other @c perform actions, or any /// Performs @c action ensuring that no other @c perform actions, or any
/// change to modals, occurs simultaneously. /// change to modals, occurs simultaneously.
void perform(const std::function<void(void)> &action); void perform(const std::function<void(void)> &action);
/// @returns new Modals if any have been set since the last call to get_new_modals(). /// @returns new Modals if any have been set since the last call to get_new_modals().
/// The caller must be within a @c perform block. /// The caller must be within a @c perform block.
const Modals *new_modals(); const Modals *new_modals();
/// @returns the current @c Modals. /// @returns the current @c Modals.
const Modals &modals() const; const Modals &modals() const;
/// @returns @c true if new modals are available; @c false otherwise. /// @returns @c true if new modals are available; @c false otherwise.
/// ///
/// Safe to call from any thread. /// Safe to call from any thread.
bool has_new_modals() const; bool has_new_modals() const;
private: private:
// ScanTarget overrides. // ScanTarget overrides.
void set_modals(Modals) final; void set_modals(Modals) final;
Outputs::Display::ScanTarget::Scan *begin_scan() final; Outputs::Display::ScanTarget::Scan *begin_scan() final;
void end_scan() final; void end_scan() final;
uint8_t *begin_data(size_t required_length, size_t required_alignment) final; uint8_t *begin_data(size_t required_length, size_t required_alignment) final;
void end_data(size_t actual_length) final; void end_data(size_t actual_length) final;
void announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) final; void announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) final;
void will_change_owner() final; void will_change_owner() final;
// Uses a texture to vend write areas. // Uses a texture to vend write areas.
uint8_t *write_area_ = nullptr; uint8_t *write_area_ = nullptr;
size_t data_type_size_ = 0; size_t data_type_size_ = 0;
// Tracks changes in raster visibility in order to populate // Tracks changes in raster visibility in order to populate
// Lines and LineMetadatas. // Lines and LineMetadatas.
bool output_is_visible_ = false; bool output_is_visible_ = false;
// Track allocation failures. // Track allocation failures.
bool data_is_allocated_ = false; bool data_is_allocated_ = false;
bool allocation_has_failed_ = false; bool allocation_has_failed_ = false;
// Ephemeral information for the begin/end functions. // Ephemeral information for the begin/end functions.
Scan *vended_scan_ = nullptr; Scan *vended_scan_ = nullptr;
int vended_write_area_pointer_ = 0; int vended_write_area_pointer_ = 0;
// Ephemeral state that helps in line composition. // Ephemeral state that helps in line composition.
int provided_scans_ = 0; int provided_scans_ = 0;
bool is_first_in_frame_ = true; bool is_first_in_frame_ = true;
bool frame_is_complete_ = true; bool frame_is_complete_ = true;
bool previous_frame_was_complete_ = true; bool previous_frame_was_complete_ = true;
// By convention everything in the PointerSet points to the next instance // By convention everything in the PointerSet points to the next instance
// of whatever it is that will be used. So a client should start with whatever // of whatever it is that will be used. So a client should start with whatever
// is pointed to by the read pointers and carry until it gets to a value that // is pointed to by the read pointers and carry until it gets to a value that
// is equal to whatever is in the submit pointers. // is equal to whatever is in the submit pointers.
struct PointerSet { struct PointerSet {
// This constructor is here to appease GCC's interpretation of // This constructor is here to appease GCC's interpretation of
// an ambiguity in the C++ standard; cf. https://stackoverflow.com/questions/17430377 // an ambiguity in the C++ standard; cf. https://stackoverflow.com/questions/17430377
PointerSet() noexcept = default; PointerSet() noexcept = default;
// Squeezing this struct into 64 bits makes the std::atomics more likely // Squeezing this struct into 64 bits makes the std::atomics more likely
// to be lock free; they are under LLVM x86-64. // to be lock free; they are under LLVM x86-64.
// Points to the vended area in the write area texture. // Points to the vended area in the write area texture.
// The vended area is always preceded by a guard pixel, so a // The vended area is always preceded by a guard pixel, so a
// sensible default construction is write_area = 1. // sensible default construction is write_area = 1.
int32_t write_area = 1; int32_t write_area = 1;
// Points into the scan buffer. // Points into the scan buffer.
uint16_t scan = 0; uint16_t scan = 0;
// Points into the line buffer. // Points into the line buffer.
uint16_t line = 0; uint16_t line = 0;
}; };
/// A pointer to the final thing currently cleared for submission. /// A pointer to the final thing currently cleared for submission.
std::atomic<PointerSet> submit_pointers_; std::atomic<PointerSet> submit_pointers_;
/// A pointer to the first thing not yet submitted for display; this is /// A pointer to the first thing not yet submitted for display; this is
/// atomic since it also acts as the buffer into which the write_pointers_ /// atomic since it also acts as the buffer into which the write_pointers_
/// may run and is therefore used by both producer and consumer. /// may run and is therefore used by both producer and consumer.
std::atomic<PointerSet> read_pointers_; std::atomic<PointerSet> read_pointers_;
std::atomic<PointerSet> read_ahead_pointers_; std::atomic<PointerSet> read_ahead_pointers_;
/// This is used as a spinlock to guard `perform` calls. /// This is used as a spinlock to guard `perform` calls.
std::atomic_flag is_updating_; std::atomic_flag is_updating_;
/// A mutex for gettng access to anything the producer modifies — i.e. the write_pointers_, /// A mutex for gettng access to anything the producer modifies — i.e. the write_pointers_,
/// data_type_size_ and write_area_texture_, and all other state to do with capturing /// data_type_size_ and write_area_texture_, and all other state to do with capturing
/// data, scans and lines. /// data, scans and lines.
/// ///
/// This is almost never contended. The main collision is a user-prompted change of modals while the /// This is almost never contended. The main collision is a user-prompted change of modals while the
/// emulation thread is running. /// emulation thread is running.
std::mutex producer_mutex_; std::mutex producer_mutex_;
/// A pointer to the next thing that should be provided to the caller for data. /// A pointer to the next thing that should be provided to the caller for data.
PointerSet write_pointers_; PointerSet write_pointers_;
// The owner-supplied scan buffer and size. // The owner-supplied scan buffer and size.
Scan *scan_buffer_ = nullptr; Scan *scan_buffer_ = nullptr;
size_t scan_buffer_size_ = 0; size_t scan_buffer_size_ = 0;
// The owner-supplied line buffer and size. // The owner-supplied line buffer and size.
Line *line_buffer_ = nullptr; Line *line_buffer_ = nullptr;
LineMetadata *line_metadata_buffer_ = nullptr; LineMetadata *line_metadata_buffer_ = nullptr;
size_t line_buffer_size_ = 0; size_t line_buffer_size_ = 0;
// Current modals and whether they've yet been returned // Current modals and whether they've yet been returned
// from a call to @c get_new_modals. // from a call to @c get_new_modals.
Modals modals_; Modals modals_;
std::atomic<bool> modals_are_dirty_ = false; std::atomic<bool> modals_are_dirty_ = false;
// Provides a per-data size implementation of end_data; a previous // Provides a per-data size implementation of end_data; a previous
// implementation used blind memcpy and that turned into something // implementation used blind memcpy and that turned into something
// of a profiling hot spot. // of a profiling hot spot.
template <typename DataUnit> void end_data(size_t actual_length); template <typename DataUnit> void end_data(size_t actual_length);
#ifndef NDEBUG #ifndef NDEBUG
// Debug features; these amount to API validation. // Debug features; these amount to API validation.
bool scan_is_ongoing_ = false; bool scan_is_ongoing_ = false;
size_t output_area_counter_ = 0; size_t output_area_counter_ = 0;
size_t output_area_next_returned_ = 0; size_t output_area_next_returned_ = 0;
#endif #endif
}; };

View File

@@ -100,52 +100,52 @@ class BufferSource {
/// ///
template <typename SourceT, bool stereo, int divider = 1> template <typename SourceT, bool stereo, int divider = 1>
struct SampleSource: public BufferSource<SourceT, stereo> { struct SampleSource: public BufferSource<SourceT, stereo> {
public: public:
template <Action action> template <Action action>
void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *target) { void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *target) {
auto &source = *static_cast<SourceT *>(this); auto &source = *static_cast<SourceT *>(this);
if constexpr (divider == 1) { if constexpr (divider == 1) {
while(number_of_samples--) { while(number_of_samples--) {
apply<action>(*target, source.level()); apply<action>(*target, source.level());
++target; ++target;
source.advance();
}
} else {
std::size_t c = 0;
// Fill in the tail of any partially-captured level.
auto level = source.level();
while(c < number_of_samples && master_divider_ != divider) {
apply<action>(target[c], level);
++c;
++master_divider_;
}
source.advance(); source.advance();
// Provide all full levels.
auto whole_steps = static_cast<int>((number_of_samples - c) / divider);
while(whole_steps--) {
fill<action>(&target[c], &target[c + divider], source.level());
c += divider;
source.advance();
}
// Provide the head of a further partial capture.
level = source.level();
master_divider_ = static_cast<int>(number_of_samples - c);
fill<action>(&target[c], &target[number_of_samples], source.level());
} }
} else {
std::size_t c = 0;
// Fill in the tail of any partially-captured level.
auto level = source.level();
while(c < number_of_samples && master_divider_ != divider) {
apply<action>(target[c], level);
++c;
++master_divider_;
}
source.advance();
// Provide all full levels.
auto whole_steps = static_cast<int>((number_of_samples - c) / divider);
while(whole_steps--) {
fill<action>(&target[c], &target[c + divider], source.level());
c += divider;
source.advance();
}
// Provide the head of a further partial capture.
level = source.level();
master_divider_ = static_cast<int>(number_of_samples - c);
fill<action>(&target[c], &target[number_of_samples], source.level());
} }
}
// TODO: use a concept here, when C++20 filters through. // TODO: use a concept here, when C++20 filters through.
// //
// Until then: sample sources should implement this. // Until then: sample sources should implement this.
// typename SampleT<stereo>::type level() const; // typename SampleT<stereo>::type level() const;
// void advance(); // void advance();
private: private:
int master_divider_{}; int master_divider_{};
}; };
} }

View File

@@ -45,139 +45,140 @@ template <typename... S> constexpr bool are_properly_ordered() {
An owner may optionally assign relative volumes. An owner may optionally assign relative volumes.
*/ */
template <typename... T> class CompoundSource: template <typename... T> class CompoundSource:
public Outputs::Speaker::BufferSource<CompoundSource<T...>, ::Outputs::Speaker::is_stereo<T...>()> { public Outputs::Speaker::BufferSource<CompoundSource<T...>, ::Outputs::Speaker::is_stereo<T...>()>
private: {
template <typename... S> class CompoundSourceHolder { private:
public: template <typename... S> class CompoundSourceHolder {
static constexpr bool is_stereo = false; public:
void set_scaled_volume_range(int16_t, double *, double) {} static constexpr bool is_stereo = false;
static constexpr std::size_t size() { return 0; } void set_scaled_volume_range(int16_t, double *, double) {}
double total_scale(double *) const { return 0.0; } static constexpr std::size_t size() { return 0; }
}; double total_scale(double *) const { return 0.0; }
};
template <typename S, typename... R> class CompoundSourceHolder<S, R...> {
public:
CompoundSourceHolder(S &source, R &...next) : source_(source), next_source_(next...) {}
static constexpr bool is_stereo = S::is_stereo || CompoundSourceHolder<R...>::is_stereo;
template <Outputs::Speaker::Action action, bool output_stereo>
void apply_samples(std::size_t number_of_samples, typename ::Outputs::Speaker::SampleT<output_stereo>::type *target) {
// If this is the step at which a mono-to-stereo adaptation happens, apply it.
if constexpr (output_stereo && !S::is_stereo) {
// There'll be only one place in the chain that this conversion happens, but it'll
// happen there often. So avoid continuously reallocating.
if(conversion_source_.size() < number_of_samples) {
conversion_source_.resize(number_of_samples);
}
// Populate the conversion buffer with this source and all below.
apply_samples<Action::Store, false>(number_of_samples, conversion_source_.data());
// Map up and return.
for(std::size_t c = 0; c < number_of_samples; c++) {
Outputs::Speaker::apply<action>(target[c], StereoSample(conversion_source_[c]));
}
} else {
constexpr bool is_final_source = sizeof...(R) == 0;
// Get the rest of the output, if any.
if constexpr (!is_final_source) {
next_source_.template apply_samples<action, output_stereo>(number_of_samples, target);
}
if(source_.is_zero_level()) {
// This component is currently outputting silence; therefore don't add anything to the output
// audio. Zero fill only if this is the final source (as everything above will be additive).
if constexpr (is_final_source) {
Outputs::Speaker::fill<action>(target, target + number_of_samples, typename SampleT<output_stereo>::type());
}
return;
}
// Add in this component's output.
source_.template apply_samples<is_final_source ? Action::Store : Action::Mix>(number_of_samples, target);
}
}
void set_scaled_volume_range(int16_t range, double *volumes, double scale) {
const auto scaled_range = volumes[0] / double(source_.average_output_peak()) * double(range) / scale;
source_.set_sample_volume_range(int16_t(scaled_range));
next_source_.set_scaled_volume_range(range, &volumes[1], scale);
}
static constexpr std::size_t size() {
return 1 + CompoundSourceHolder<R...>::size();
}
double total_scale(double *volumes) const {
return (volumes[0] / source_.average_output_peak()) + next_source_.total_scale(&volumes[1]);
}
private:
S &source_;
CompoundSourceHolder<R...> next_source_;
std::vector<MonoSample> conversion_source_;
};
template <typename S, typename... R> class CompoundSourceHolder<S, R...> {
public: public:
using Sample = typename SampleT<::Outputs::Speaker::is_stereo<T...>()>::type; CompoundSourceHolder(S &source, R &...next) : source_(source), next_source_(next...) {}
// To ensure at most one mono to stereo conversion, require appropriate source ordering. static constexpr bool is_stereo = S::is_stereo || CompoundSourceHolder<R...>::is_stereo;
static_assert(are_properly_ordered<T...>(), "Sources should be listed with all stereo sources before all mono sources");
CompoundSource(T &... sources) : source_holder_(sources...) { template <Outputs::Speaker::Action action, bool output_stereo>
// Default: give all sources equal volume. void apply_samples(std::size_t number_of_samples, typename ::Outputs::Speaker::SampleT<output_stereo>::type *target) {
const auto volume = 1.0 / double(source_holder_.size());
for(std::size_t c = 0; c < source_holder_.size(); ++c) { // If this is the step at which a mono-to-stereo adaptation happens, apply it.
volumes_.push_back(volume); if constexpr (output_stereo && !S::is_stereo) {
// There'll be only one place in the chain that this conversion happens, but it'll
// happen there often. So avoid continuously reallocating.
if(conversion_source_.size() < number_of_samples) {
conversion_source_.resize(number_of_samples);
}
// Populate the conversion buffer with this source and all below.
apply_samples<Action::Store, false>(number_of_samples, conversion_source_.data());
// Map up and return.
for(std::size_t c = 0; c < number_of_samples; c++) {
Outputs::Speaker::apply<action>(target[c], StereoSample(conversion_source_[c]));
}
} else {
constexpr bool is_final_source = sizeof...(R) == 0;
// Get the rest of the output, if any.
if constexpr (!is_final_source) {
next_source_.template apply_samples<action, output_stereo>(number_of_samples, target);
}
if(source_.is_zero_level()) {
// This component is currently outputting silence; therefore don't add anything to the output
// audio. Zero fill only if this is the final source (as everything above will be additive).
if constexpr (is_final_source) {
Outputs::Speaker::fill<action>(target, target + number_of_samples, typename SampleT<output_stereo>::type());
}
return;
}
// Add in this component's output.
source_.template apply_samples<is_final_source ? Action::Store : Action::Mix>(number_of_samples, target);
} }
} }
template <Outputs::Speaker::Action action> void set_scaled_volume_range(int16_t range, double *volumes, double scale) {
void apply_samples(std::size_t number_of_samples, Sample *target) { const auto scaled_range = volumes[0] / double(source_.average_output_peak()) * double(range) / scale;
source_holder_.template apply_samples<action, ::Outputs::Speaker::is_stereo<T...>()>(number_of_samples, target); source_.set_sample_volume_range(int16_t(scaled_range));
next_source_.set_scaled_volume_range(range, &volumes[1], scale);
} }
/*! static constexpr std::size_t size() {
Sets the total output volume of this CompoundSource. return 1 + CompoundSourceHolder<R...>::size();
*/
void set_sample_volume_range(int16_t range) {
volume_range_ = range;
push_volumes();
} }
/*! double total_scale(double *volumes) const {
Sets the relative volumes of the various sources underlying this return (volumes[0] / source_.average_output_peak()) + next_source_.total_scale(&volumes[1]);
compound. The caller should ensure that the number of items supplied
matches the number of sources and that the values in it sum to 1.0.
*/
void set_relative_volumes(const std::vector<double> &volumes) {
assert(volumes.size() == source_holder_.size());
volumes_ = volumes;
push_volumes();
average_output_peak_ = 1.0 / source_holder_.total_scale(volumes_.data());
}
/*!
@returns the average output peak given the sources owned by this CompoundSource and the
current relative volumes.
*/
double average_output_peak() const {
return average_output_peak_;
} }
private: private:
void push_volumes() { S &source_;
const double scale = source_holder_.total_scale(volumes_.data()); CompoundSourceHolder<R...> next_source_;
source_holder_.set_scaled_volume_range(volume_range_, volumes_.data(), scale); std::vector<MonoSample> conversion_source_;
} };
CompoundSourceHolder<T...> source_holder_; public:
std::vector<double> volumes_; using Sample = typename SampleT<::Outputs::Speaker::is_stereo<T...>()>::type;
int16_t volume_range_ = 0;
std::atomic<double> average_output_peak_{1.0}; // To ensure at most one mono to stereo conversion, require appropriate source ordering.
static_assert(are_properly_ordered<T...>(), "Sources should be listed with all stereo sources before all mono sources");
CompoundSource(T &... sources) : source_holder_(sources...) {
// Default: give all sources equal volume.
const auto volume = 1.0 / double(source_holder_.size());
for(std::size_t c = 0; c < source_holder_.size(); ++c) {
volumes_.push_back(volume);
}
}
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Sample *target) {
source_holder_.template apply_samples<action, ::Outputs::Speaker::is_stereo<T...>()>(number_of_samples, target);
}
/*!
Sets the total output volume of this CompoundSource.
*/
void set_sample_volume_range(int16_t range) {
volume_range_ = range;
push_volumes();
}
/*!
Sets the relative volumes of the various sources underlying this
compound. The caller should ensure that the number of items supplied
matches the number of sources and that the values in it sum to 1.0.
*/
void set_relative_volumes(const std::vector<double> &volumes) {
assert(volumes.size() == source_holder_.size());
volumes_ = volumes;
push_volumes();
average_output_peak_ = 1.0 / source_holder_.total_scale(volumes_.data());
}
/*!
@returns the average output peak given the sources owned by this CompoundSource and the
current relative volumes.
*/
double average_output_peak() const {
return average_output_peak_;
}
private:
void push_volumes() {
const double scale = source_holder_.total_scale(volumes_.data());
source_holder_.set_scaled_volume_range(volume_range_, volumes_.data(), scale);
}
CompoundSourceHolder<T...> source_holder_;
std::vector<double> volumes_;
int16_t volume_range_ = 0;
std::atomic<double> average_output_peak_{1.0};
}; };
} }

View File

@@ -23,324 +23,324 @@
namespace Outputs::Speaker { namespace Outputs::Speaker {
template <typename ConcreteT, bool is_stereo> class LowpassBase: public Speaker { template <typename ConcreteT, bool is_stereo> class LowpassBase: public Speaker {
public: public:
/*! /*!
Sets the clock rate of the input audio. Sets the clock rate of the input audio.
*/ */
void set_input_rate(float cycles_per_second) { void set_input_rate(float cycles_per_second) {
std::lock_guard lock_guard(filter_parameters_mutex_); std::lock_guard lock_guard(filter_parameters_mutex_);
if(filter_parameters_.input_cycles_per_second == cycles_per_second) { if(filter_parameters_.input_cycles_per_second == cycles_per_second) {
return; return;
} }
filter_parameters_.input_cycles_per_second = cycles_per_second; filter_parameters_.input_cycles_per_second = cycles_per_second;
filter_parameters_.parameters_are_dirty = true; filter_parameters_.parameters_are_dirty = true;
filter_parameters_.input_rate_changed = true; filter_parameters_.input_rate_changed = true;
}
/*!
Allows a cut-off frequency to be specified for audio. Ordinarily this low-pass speaker
will determine a cut-off based on the output audio rate. A caller can manually select
an alternative cut-off. This allows machines with a low-pass filter on their audio output
path to be explicit about its effect, and get that simulation for free.
*/
void set_high_frequency_cutoff(float high_frequency) {
std::lock_guard lock_guard(filter_parameters_mutex_);
if(filter_parameters_.high_frequency_cutoff == high_frequency) {
return;
}
filter_parameters_.high_frequency_cutoff = high_frequency;
filter_parameters_.parameters_are_dirty = true;
}
private:
float get_ideal_clock_rate_in_range(float minimum, float maximum) final {
std::lock_guard lock_guard(filter_parameters_mutex_);
// Return twice the cut off, if applicable.
if( filter_parameters_.high_frequency_cutoff > 0.0f &&
filter_parameters_.input_cycles_per_second >= filter_parameters_.high_frequency_cutoff * 3.0f &&
filter_parameters_.input_cycles_per_second <= filter_parameters_.high_frequency_cutoff * 3.0f)
return filter_parameters_.high_frequency_cutoff * 3.0f;
// Return exactly the input rate if possible.
if( filter_parameters_.input_cycles_per_second >= minimum &&
filter_parameters_.input_cycles_per_second <= maximum)
return filter_parameters_.input_cycles_per_second;
// If the input rate is lower, return the minimum...
if(filter_parameters_.input_cycles_per_second < minimum)
return minimum;
// ... otherwise, return the maximum.
return maximum;
}
// Implemented as per Speaker.
void set_computed_output_rate(float cycles_per_second, int buffer_size, bool) final {
std::lock_guard lock_guard(filter_parameters_mutex_);
if(filter_parameters_.output_cycles_per_second == cycles_per_second && size_t(buffer_size) == output_buffer_.size()) {
return;
} }
/*! filter_parameters_.output_cycles_per_second = cycles_per_second;
Allows a cut-off frequency to be specified for audio. Ordinarily this low-pass speaker filter_parameters_.parameters_are_dirty = true;
will determine a cut-off based on the output audio rate. A caller can manually select output_buffer_.resize(std::size_t(buffer_size) * (is_stereo + 1));
an alternative cut-off. This allows machines with a low-pass filter on their audio output }
path to be explicit about its effect, and get that simulation for free.
*/ // MARK: - Filtering.
void set_high_frequency_cutoff(float high_frequency) {
std::lock_guard lock_guard(filter_parameters_mutex_); std::size_t output_buffer_pointer_ = 0;
if(filter_parameters_.high_frequency_cutoff == high_frequency) { std::size_t input_buffer_depth_ = 0;
return; std::vector<int16_t> input_buffer_;
} std::vector<int16_t> output_buffer_;
filter_parameters_.high_frequency_cutoff = high_frequency;
filter_parameters_.parameters_are_dirty = true; float step_rate_ = 0.0f;
float position_error_ = 0.0f;
std::unique_ptr<SignalProcessing::FIRFilter> filter_;
std::mutex filter_parameters_mutex_;
struct FilterParameters {
float input_cycles_per_second = 0.0f;
float output_cycles_per_second = 0.0f;
float high_frequency_cutoff = -1.0;
bool parameters_are_dirty = true;
bool input_rate_changed = false;
} filter_parameters_;
void update_filter_coefficients(const FilterParameters &filter_parameters) {
float high_pass_frequency = filter_parameters.output_cycles_per_second / 2.0f;
if(filter_parameters.high_frequency_cutoff > 0.0) {
high_pass_frequency = std::min(filter_parameters.high_frequency_cutoff, high_pass_frequency);
} }
private: // Make a guess at a good number of taps.
float get_ideal_clock_rate_in_range(float minimum, float maximum) final { std::size_t number_of_taps = std::size_t(
std::lock_guard lock_guard(filter_parameters_mutex_); ceilf((filter_parameters.input_cycles_per_second + high_pass_frequency) / high_pass_frequency)
);
number_of_taps = (number_of_taps * 2) | 1;
// Return twice the cut off, if applicable. step_rate_ = filter_parameters.input_cycles_per_second / filter_parameters.output_cycles_per_second;
if( filter_parameters_.high_frequency_cutoff > 0.0f && position_error_ = 0.0f;
filter_parameters_.input_cycles_per_second >= filter_parameters_.high_frequency_cutoff * 3.0f &&
filter_parameters_.input_cycles_per_second <= filter_parameters_.high_frequency_cutoff * 3.0f)
return filter_parameters_.high_frequency_cutoff * 3.0f;
// Return exactly the input rate if possible. filter_ = std::make_unique<SignalProcessing::FIRFilter>(
if( filter_parameters_.input_cycles_per_second >= minimum && unsigned(number_of_taps),
filter_parameters_.input_cycles_per_second <= maximum) filter_parameters.input_cycles_per_second,
return filter_parameters_.input_cycles_per_second; 0.0,
high_pass_frequency,
SignalProcessing::FIRFilter::DefaultAttenuation);
// If the input rate is lower, return the minimum... // Pick the new conversion function.
if(filter_parameters_.input_cycles_per_second < minimum) if( filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second &&
return minimum; filter_parameters.high_frequency_cutoff < 0.0) {
// If input and output rates exactly match, and no additional cut-off has been specified,
// ... otherwise, return the maximum. // just accumulate results and pass on.
return maximum; conversion_ = Conversion::Copy;
} else if( filter_parameters.input_cycles_per_second > filter_parameters.output_cycles_per_second ||
(filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && filter_parameters.high_frequency_cutoff >= 0.0)) {
// If the output rate is less than the input rate, or an additional cut-off has been specified, use the filter.
conversion_ = Conversion::ResampleSmaller;
} else {
conversion_ = Conversion::ResampleLarger;
} }
// Implemented as per Speaker. // Do something sensible with any dangling input, if necessary.
void set_computed_output_rate(float cycles_per_second, int buffer_size, bool) final { const int scale = static_cast<ConcreteT *>(this)->get_scale();
std::lock_guard lock_guard(filter_parameters_mutex_); switch(conversion_) {
if(filter_parameters_.output_cycles_per_second == cycles_per_second && size_t(buffer_size) == output_buffer_.size()) { // Neither direct copying nor resampling larger currently use any temporary input.
return; // Although in the latter case that's just because it's unimplemented. But, regardless,
} // that means nothing to do.
default: break;
filter_parameters_.output_cycles_per_second = cycles_per_second; case Conversion::ResampleSmaller: {
filter_parameters_.parameters_are_dirty = true; // Reize the input buffer only if absolutely necessary; if sizing downward
output_buffer_.resize(std::size_t(buffer_size) * (is_stereo + 1)); // such that a sample would otherwise be lost then output it now. Keep anything
} // currently in the input buffer that hasn't yet been processed.
const size_t required_buffer_size = size_t(number_of_taps) * (is_stereo + 1);
// MARK: - Filtering. if(input_buffer_.size() != required_buffer_size) {
if(input_buffer_depth_ >= required_buffer_size) {
std::size_t output_buffer_pointer_ = 0; resample_input_buffer(scale);
std::size_t input_buffer_depth_ = 0; input_buffer_depth_ %= required_buffer_size;
std::vector<int16_t> input_buffer_;
std::vector<int16_t> output_buffer_;
float step_rate_ = 0.0f;
float position_error_ = 0.0f;
std::unique_ptr<SignalProcessing::FIRFilter> filter_;
std::mutex filter_parameters_mutex_;
struct FilterParameters {
float input_cycles_per_second = 0.0f;
float output_cycles_per_second = 0.0f;
float high_frequency_cutoff = -1.0;
bool parameters_are_dirty = true;
bool input_rate_changed = false;
} filter_parameters_;
void update_filter_coefficients(const FilterParameters &filter_parameters) {
float high_pass_frequency = filter_parameters.output_cycles_per_second / 2.0f;
if(filter_parameters.high_frequency_cutoff > 0.0) {
high_pass_frequency = std::min(filter_parameters.high_frequency_cutoff, high_pass_frequency);
}
// Make a guess at a good number of taps.
std::size_t number_of_taps = std::size_t(
ceilf((filter_parameters.input_cycles_per_second + high_pass_frequency) / high_pass_frequency)
);
number_of_taps = (number_of_taps * 2) | 1;
step_rate_ = filter_parameters.input_cycles_per_second / filter_parameters.output_cycles_per_second;
position_error_ = 0.0f;
filter_ = std::make_unique<SignalProcessing::FIRFilter>(
unsigned(number_of_taps),
filter_parameters.input_cycles_per_second,
0.0,
high_pass_frequency,
SignalProcessing::FIRFilter::DefaultAttenuation);
// Pick the new conversion function.
if( filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second &&
filter_parameters.high_frequency_cutoff < 0.0) {
// If input and output rates exactly match, and no additional cut-off has been specified,
// just accumulate results and pass on.
conversion_ = Conversion::Copy;
} else if( filter_parameters.input_cycles_per_second > filter_parameters.output_cycles_per_second ||
(filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && filter_parameters.high_frequency_cutoff >= 0.0)) {
// If the output rate is less than the input rate, or an additional cut-off has been specified, use the filter.
conversion_ = Conversion::ResampleSmaller;
} else {
conversion_ = Conversion::ResampleLarger;
}
// Do something sensible with any dangling input, if necessary.
const int scale = static_cast<ConcreteT *>(this)->get_scale();
switch(conversion_) {
// Neither direct copying nor resampling larger currently use any temporary input.
// Although in the latter case that's just because it's unimplemented. But, regardless,
// that means nothing to do.
default: break;
case Conversion::ResampleSmaller: {
// Reize the input buffer only if absolutely necessary; if sizing downward
// such that a sample would otherwise be lost then output it now. Keep anything
// currently in the input buffer that hasn't yet been processed.
const size_t required_buffer_size = size_t(number_of_taps) * (is_stereo + 1);
if(input_buffer_.size() != required_buffer_size) {
if(input_buffer_depth_ >= required_buffer_size) {
resample_input_buffer(scale);
input_buffer_depth_ %= required_buffer_size;
}
input_buffer_.resize(required_buffer_size);
} }
} break; input_buffer_.resize(required_buffer_size);
} }
} break;
}
}
inline void resample_input_buffer(int scale) {
if(output_buffer_.empty()) {
return;
} }
inline void resample_input_buffer(int scale) { if constexpr (is_stereo) {
if(output_buffer_.empty()) { output_buffer_[output_buffer_pointer_ + 0] = filter_->apply(input_buffer_.data(), 2);
return; output_buffer_[output_buffer_pointer_ + 1] = filter_->apply(input_buffer_.data() + 1, 2);
} output_buffer_pointer_+= 2;
} else {
output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data());
output_buffer_pointer_++;
}
// Apply scale, if supplied, clamping appropriately.
if(scale != 65536) {
#define SCALE(x) x = int16_t(std::clamp((int(x) * scale) >> 16, -32768, 32767))
if constexpr (is_stereo) { if constexpr (is_stereo) {
output_buffer_[output_buffer_pointer_ + 0] = filter_->apply(input_buffer_.data(), 2); SCALE(output_buffer_[output_buffer_pointer_ - 2]);
output_buffer_[output_buffer_pointer_ + 1] = filter_->apply(input_buffer_.data() + 1, 2); SCALE(output_buffer_[output_buffer_pointer_ - 1]);
output_buffer_pointer_+= 2;
} else { } else {
output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data()); SCALE(output_buffer_[output_buffer_pointer_ - 1]);
output_buffer_pointer_++;
}
// Apply scale, if supplied, clamping appropriately.
if(scale != 65536) {
#define SCALE(x) x = int16_t(std::clamp((int(x) * scale) >> 16, -32768, 32767))
if constexpr (is_stereo) {
SCALE(output_buffer_[output_buffer_pointer_ - 2]);
SCALE(output_buffer_[output_buffer_pointer_ - 1]);
} else {
SCALE(output_buffer_[output_buffer_pointer_ - 1]);
}
#undef SCALE
}
// Announce to delegate if full.
if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0;
did_complete_samples(this, output_buffer_, is_stereo);
}
// If the next loop around is going to reuse some of the samples just collected, use a memmove to
// preserve them in the correct locations (TODO: use a longer buffer to fix that?) and don't skip
// anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse.
const size_t steps = size_t(step_rate_ + position_error_) * (is_stereo + 1);
position_error_ = fmodf(step_rate_ + position_error_, 1.0f);
if(steps < input_buffer_.size()) {
auto *const input_buffer = input_buffer_.data();
std::memmove( input_buffer,
&input_buffer[steps],
sizeof(int16_t) * (input_buffer_.size() - steps));
input_buffer_depth_ -= steps;
} else {
if(steps > input_buffer_.size()) {
static_cast<ConcreteT *>(this)->skip_samples((steps - input_buffer_.size()) / (1 + is_stereo));
}
input_buffer_depth_ = 0;
} }
#undef SCALE
} }
enum class Conversion { // Announce to delegate if full.
ResampleSmaller, if(output_buffer_pointer_ == output_buffer_.size()) {
Copy, output_buffer_pointer_ = 0;
ResampleLarger did_complete_samples(this, output_buffer_, is_stereo);
} conversion_ = Conversion::Copy;
bool recalculate_filter_if_dirty() {
FilterParameters filter_parameters;
{
std::lock_guard lock_guard(filter_parameters_mutex_);
filter_parameters = filter_parameters_;
filter_parameters_.parameters_are_dirty = false;
filter_parameters_.input_rate_changed = false;
}
if(filter_parameters.parameters_are_dirty) update_filter_coefficients(filter_parameters);
return filter_parameters.input_rate_changed;
} }
protected: // If the next loop around is going to reuse some of the samples just collected, use a memmove to
bool process(size_t length) { // preserve them in the correct locations (TODO: use a longer buffer to fix that?) and don't skip
const auto delegate = delegate_.load(std::memory_order_relaxed); // anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse.
if(!delegate) return false; const size_t steps = size_t(step_rate_ + position_error_) * (is_stereo + 1);
position_error_ = fmodf(step_rate_ + position_error_, 1.0f);
const int scale = static_cast<ConcreteT *>(this)->get_scale(); if(steps < input_buffer_.size()) {
auto *const input_buffer = input_buffer_.data();
if(recalculate_filter_if_dirty()) { std::memmove( input_buffer,
delegate->speaker_did_change_input_clock(this); &input_buffer[steps],
sizeof(int16_t) * (input_buffer_.size() - steps));
input_buffer_depth_ -= steps;
} else {
if(steps > input_buffer_.size()) {
static_cast<ConcreteT *>(this)->skip_samples((steps - input_buffer_.size()) / (1 + is_stereo));
} }
input_buffer_depth_ = 0;
}
}
switch(conversion_) { enum class Conversion {
case Conversion::Copy: ResampleSmaller,
while(length) { Copy,
const auto samples_to_read = std::min((output_buffer_.size() - output_buffer_pointer_) / (1 + is_stereo), length); ResampleLarger
static_cast<ConcreteT *>(this)->get_samples(samples_to_read, &output_buffer_[output_buffer_pointer_ ]); } conversion_ = Conversion::Copy;
output_buffer_pointer_ += samples_to_read * (1 + is_stereo);
// TODO: apply scale. bool recalculate_filter_if_dirty() {
FilterParameters filter_parameters;
{
std::lock_guard lock_guard(filter_parameters_mutex_);
filter_parameters = filter_parameters_;
filter_parameters_.parameters_are_dirty = false;
filter_parameters_.input_rate_changed = false;
}
if(filter_parameters.parameters_are_dirty) update_filter_coefficients(filter_parameters);
return filter_parameters.input_rate_changed;
}
// Announce to delegate if full. protected:
if(output_buffer_pointer_ == output_buffer_.size()) { bool process(size_t length) {
output_buffer_pointer_ = 0; const auto delegate = delegate_.load(std::memory_order_relaxed);
did_complete_samples(this, output_buffer_, is_stereo); if(!delegate) return false;
}
length -= samples_to_read; const int scale = static_cast<ConcreteT *>(this)->get_scale();
if(recalculate_filter_if_dirty()) {
delegate->speaker_did_change_input_clock(this);
}
switch(conversion_) {
case Conversion::Copy:
while(length) {
const auto samples_to_read = std::min((output_buffer_.size() - output_buffer_pointer_) / (1 + is_stereo), length);
static_cast<ConcreteT *>(this)->get_samples(samples_to_read, &output_buffer_[output_buffer_pointer_ ]);
output_buffer_pointer_ += samples_to_read * (1 + is_stereo);
// TODO: apply scale.
// Announce to delegate if full.
if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0;
did_complete_samples(this, output_buffer_, is_stereo);
} }
break;
case Conversion::ResampleSmaller: length -= samples_to_read;
while(length) { }
const auto cycles_to_read = std::min((input_buffer_.size() - input_buffer_depth_) / (1 + is_stereo), length); break;
static_cast<ConcreteT *>(this)->get_samples(cycles_to_read, &input_buffer_[input_buffer_depth_]);
input_buffer_depth_ += cycles_to_read * (1 + is_stereo);
if(input_buffer_depth_ == input_buffer_.size()) { case Conversion::ResampleSmaller:
resample_input_buffer(scale); while(length) {
} const auto cycles_to_read = std::min((input_buffer_.size() - input_buffer_depth_) / (1 + is_stereo), length);
static_cast<ConcreteT *>(this)->get_samples(cycles_to_read, &input_buffer_[input_buffer_depth_]);
input_buffer_depth_ += cycles_to_read * (1 + is_stereo);
length -= cycles_to_read; if(input_buffer_depth_ == input_buffer_.size()) {
resample_input_buffer(scale);
} }
break;
case Conversion::ResampleLarger: length -= cycles_to_read;
// TODO: input rate is less than output rate. }
break; break;
}
return true; case Conversion::ResampleLarger:
// TODO: input rate is less than output rate.
break;
} }
return true;
}
}; };
/*! /*!
Provides a low-pass speaker to which blocks of samples are pushed. Provides a low-pass speaker to which blocks of samples are pushed.
*/ */
template <bool is_stereo> class PushLowpass: public LowpassBase<PushLowpass<is_stereo>, is_stereo> { template <bool is_stereo> class PushLowpass: public LowpassBase<PushLowpass<is_stereo>, is_stereo> {
private: private:
using BaseT = LowpassBase<PushLowpass<is_stereo>, is_stereo>; using BaseT = LowpassBase<PushLowpass<is_stereo>, is_stereo>;
friend BaseT; friend BaseT;
using BaseT::process; using BaseT::process;
std::atomic<int> scale_ = 65536; std::atomic<int> scale_ = 65536;
int get_scale() const { int get_scale() const {
return scale_.load(std::memory_order_relaxed); return scale_.load(std::memory_order_relaxed);
} }
const int16_t *buffer_ = nullptr; const int16_t *buffer_ = nullptr;
void skip_samples(size_t count) { void skip_samples(size_t count) {
buffer_ += count; buffer_ += count;
} }
void get_samples(size_t length, int16_t *target) { void get_samples(size_t length, int16_t *target) {
const auto word_length = length * (1 + is_stereo); const auto word_length = length * (1 + is_stereo);
memcpy(target, buffer_, word_length * sizeof(int16_t)); memcpy(target, buffer_, word_length * sizeof(int16_t));
buffer_ += word_length; buffer_ += word_length;
} }
public: public:
void set_output_volume(float volume) final { void set_output_volume(float volume) final {
scale_.store(int(std::clamp(volume * 65536.0f, 0.0f, 65536.0f))); scale_.store(int(std::clamp(volume * 65536.0f, 0.0f, 65536.0f)));
} }
bool get_is_stereo() final { bool get_is_stereo() final {
return is_stereo; return is_stereo;
} }
/*! /*!
Filters and posts onward the provided buffer, on the calling thread. Filters and posts onward the provided buffer, on the calling thread.
@param buffer The source for samples. @param buffer The source for samples.
@param length The number of samples provided; in mono this will be the number of int16_ts @param length The number of samples provided; in mono this will be the number of int16_ts
it is safe to read from @c buffer, and in stereo it will be half the number — it is a count it is safe to read from @c buffer, and in stereo it will be half the number — it is a count
of the number of time points at which audio was sampled. of the number of time points at which audio was sampled.
*/ */
void push(const int16_t *buffer, size_t length) { void push(const int16_t *buffer, size_t length) {
buffer_ = buffer; buffer_ = buffer;
#ifndef NDEBUG #ifndef NDEBUG
const bool did_process = const bool did_process =
#endif #endif
process(length); process(length);
assert(!did_process || buffer_ == buffer + (length * (1 + is_stereo))); assert(!did_process || buffer_ == buffer + (length * (1 + is_stereo)));
} }
}; };
/*! /*!
@@ -350,68 +350,68 @@ template <bool is_stereo> class PushLowpass: public LowpassBase<PushLowpass<is_s
lower-frequency output. lower-frequency output.
*/ */
template <typename SampleSource> class PullLowpass: public LowpassBase<PullLowpass<SampleSource>, SampleSource::is_stereo> { template <typename SampleSource> class PullLowpass: public LowpassBase<PullLowpass<SampleSource>, SampleSource::is_stereo> {
public: public:
PullLowpass(SampleSource &sample_source) : sample_source_(sample_source) { PullLowpass(SampleSource &sample_source) : sample_source_(sample_source) {
// Propagate an initial volume level. // Propagate an initial volume level.
sample_source.set_sample_volume_range(32767); sample_source.set_sample_volume_range(32767);
}
void set_output_volume(float volume) final {
// Clamp to the acceptable range, and set.
volume = std::clamp(volume, 0.0f, 1.0f);
sample_source_.set_sample_volume_range(int16_t(32767.0f * volume));
}
bool get_is_stereo() final {
return SampleSource::is_stereo;
}
/*!
Schedules an advancement by the number of cycles specified on the provided queue.
The speaker will advance by obtaining data from the sample source supplied
at construction, filtering it and passing it on to the speaker's delegate if there is one.
*/
void run_for(Concurrency::AsyncTaskQueue<false> &queue, const Cycles cycles) {
if(cycles == Cycles(0)) {
return;
} }
void set_output_volume(float volume) final { queue.enqueue([this, cycles] {
// Clamp to the acceptable range, and set. run_for(cycles);
volume = std::clamp(volume, 0.0f, 1.0f); });
sample_source_.set_sample_volume_range(int16_t(32767.0f * volume)); }
}
private:
bool get_is_stereo() final { using BaseT = LowpassBase<PullLowpass<SampleSource>, SampleSource::is_stereo>;
return SampleSource::is_stereo; friend BaseT;
} using BaseT::process;
/*! /*!
Schedules an advancement by the number of cycles specified on the provided queue. Advances by the number of cycles specified, obtaining data from the sample source supplied
The speaker will advance by obtaining data from the sample source supplied at construction, filtering it and passing it on to the speaker's delegate if there is one.
at construction, filtering it and passing it on to the speaker's delegate if there is one. */
*/ void run_for(const Cycles cycles) {
void run_for(Concurrency::AsyncTaskQueue<false> &queue, const Cycles cycles) { process(size_t(cycles.as_integral()));
if(cycles == Cycles(0)) { }
return;
} SampleSource &sample_source_;
queue.enqueue([this, cycles] { void skip_samples(size_t count) {
run_for(cycles); sample_source_.template apply_samples<Action::Ignore>(count, nullptr);
}); }
}
int get_scale() {
private: return int(65536.0 / sample_source_.average_output_peak());
using BaseT = LowpassBase<PullLowpass<SampleSource>, SampleSource::is_stereo>; }
friend BaseT;
using BaseT::process; void get_samples(size_t length, int16_t *target) {
if constexpr (SampleSource::is_stereo) {
/*! StereoSample *const stereo_target = reinterpret_cast<StereoSample *>(target);
Advances by the number of cycles specified, obtaining data from the sample source supplied sample_source_.template apply_samples<Action::Store>(length, stereo_target);
at construction, filtering it and passing it on to the speaker's delegate if there is one. } else {
*/ sample_source_.template apply_samples<Action::Store>(length, target);
void run_for(const Cycles cycles) {
process(size_t(cycles.as_integral()));
}
SampleSource &sample_source_;
void skip_samples(size_t count) {
sample_source_.template apply_samples<Action::Ignore>(count, nullptr);
}
int get_scale() {
return int(65536.0 / sample_source_.average_output_peak());
}
void get_samples(size_t length, int16_t *target) {
if constexpr (SampleSource::is_stereo) {
StereoSample *const stereo_target = reinterpret_cast<StereoSample *>(target);
sample_source_.template apply_samples<Action::Store>(length, stereo_target);
} else {
sample_source_.template apply_samples<Action::Store>(length, target);
}
} }
}
}; };
} }

View File

@@ -61,128 +61,128 @@ template <bool stereo> struct SampleT {
audio output. audio output.
*/ */
class Speaker { class Speaker {
public: public:
virtual ~Speaker() = default; virtual ~Speaker() = default;
/*!
@returns The best output clock rate for the audio being supplied to this speaker, from the range given.
*/
virtual float get_ideal_clock_rate_in_range(float minimum, float maximum) = 0;
/*!
@returns @c true if the device would most ideally output stereo sound; @c false otherwise.
*/
virtual bool get_is_stereo() = 0;
/*!
Sets the actual output rate; packets provided to the delegate will conform to these
specifications regardless of the input.
*/
void set_output_rate(float cycles_per_second, int buffer_size, bool stereo) {
output_cycles_per_second_ = cycles_per_second;
output_buffer_size_ = buffer_size;
stereo_output_ = stereo;
compute_output_rate();
}
/*!
Takes a copy of the most recent output rate provided to @c rhs.
*/
void copy_output_rate(const Speaker &rhs) {
output_cycles_per_second_ = rhs.output_cycles_per_second_;
output_buffer_size_ = rhs.output_buffer_size_;
stereo_output_.store(rhs.stereo_output_.load(std::memory_order_relaxed), std::memory_order_relaxed);
compute_output_rate();
}
/// Sets the output volume, in the range [0, 1].
virtual void set_output_volume(float) = 0;
/*!
Speeds a speed multiplier for this machine, e.g. that it is currently being run at 2.0x its normal rate.
This will affect the number of input samples that are combined to produce one output sample.
*/
void set_input_rate_multiplier(float multiplier) {
input_rate_multiplier_ = multiplier;
compute_output_rate();
}
/*!
@returns The number of sample sets so far delivered to the delegate.
*/
int completed_sample_sets() const { return completed_sample_sets_; }
/*!
Defines a receiver for audio packets.
*/
struct Delegate {
/*!
Indicates that a new audio packet is ready. If the output is stereo, samples will be interleaved with the first
being left, the second being right, etc.
*/
virtual void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) = 0;
/*! /*!
@returns The best output clock rate for the audio being supplied to this speaker, from the range given. Provides the delegate with a hint that the input clock rate has changed, which provides an opportunity to
renegotiate the ideal clock rate, if desired.
*/ */
virtual float get_ideal_clock_rate_in_range(float minimum, float maximum) = 0; virtual void speaker_did_change_input_clock([[maybe_unused]] Speaker *speaker) {}
};
virtual void set_delegate(Delegate *delegate) {
delegate_.store(delegate, std::memory_order_relaxed);
}
/*!
@returns @c true if the device would most ideally output stereo sound; @c false otherwise.
*/
virtual bool get_is_stereo() = 0;
/*! // This is primarily exposed for MultiSpeaker et al; it's not for general callers.
Sets the actual output rate; packets provided to the delegate will conform to these virtual void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) = 0;
specifications regardless of the input.
*/ protected:
void set_output_rate(float cycles_per_second, int buffer_size, bool stereo) { void did_complete_samples(Speaker *, const std::vector<int16_t> &buffer, bool is_stereo) {
output_cycles_per_second_ = cycles_per_second; // Test the delegate for existence again, as it may have changed.
output_buffer_size_ = buffer_size; const auto delegate = delegate_.load(std::memory_order_relaxed);
stereo_output_ = stereo; if(!delegate) return;
compute_output_rate();
++completed_sample_sets_;
// Hope for the fast path first: producer and consumer agree about
// number of channels.
if(is_stereo == stereo_output_) {
delegate->speaker_did_complete_samples(this, buffer);
return;
} }
/*! // Producer and consumer don't agree, so mix two channels to one, or double out one to two.
Takes a copy of the most recent output rate provided to @c rhs. if(is_stereo) {
*/ // Mix down.
void copy_output_rate(const Speaker &rhs) { mix_buffer_.resize(buffer.size() / 2);
output_cycles_per_second_ = rhs.output_cycles_per_second_; for(size_t c = 0; c < mix_buffer_.size(); ++c) {
output_buffer_size_ = rhs.output_buffer_size_; mix_buffer_[c] = (buffer[(c << 1) + 0] + buffer[(c << 1) + 1]) >> 1;
stereo_output_.store(rhs.stereo_output_.load(std::memory_order_relaxed), std::memory_order_relaxed); // TODO: is there an Accelerate framework solution to this?
compute_output_rate();
}
/// Sets the output volume, in the range [0, 1].
virtual void set_output_volume(float) = 0;
/*!
Speeds a speed multiplier for this machine, e.g. that it is currently being run at 2.0x its normal rate.
This will affect the number of input samples that are combined to produce one output sample.
*/
void set_input_rate_multiplier(float multiplier) {
input_rate_multiplier_ = multiplier;
compute_output_rate();
}
/*!
@returns The number of sample sets so far delivered to the delegate.
*/
int completed_sample_sets() const { return completed_sample_sets_; }
/*!
Defines a receiver for audio packets.
*/
struct Delegate {
/*!
Indicates that a new audio packet is ready. If the output is stereo, samples will be interleaved with the first
being left, the second being right, etc.
*/
virtual void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) = 0;
/*!
Provides the delegate with a hint that the input clock rate has changed, which provides an opportunity to
renegotiate the ideal clock rate, if desired.
*/
virtual void speaker_did_change_input_clock([[maybe_unused]] Speaker *speaker) {}
};
virtual void set_delegate(Delegate *delegate) {
delegate_.store(delegate, std::memory_order_relaxed);
}
// This is primarily exposed for MultiSpeaker et al; it's not for general callers.
virtual void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) = 0;
protected:
void did_complete_samples(Speaker *, const std::vector<int16_t> &buffer, bool is_stereo) {
// Test the delegate for existence again, as it may have changed.
const auto delegate = delegate_.load(std::memory_order_relaxed);
if(!delegate) return;
++completed_sample_sets_;
// Hope for the fast path first: producer and consumer agree about
// number of channels.
if(is_stereo == stereo_output_) {
delegate->speaker_did_complete_samples(this, buffer);
return;
} }
} else {
// Producer and consumer don't agree, so mix two channels to one, or double out one to two. // Double up.
if(is_stereo) { mix_buffer_.resize(buffer.size() * 2);
// Mix down. for(size_t c = 0; c < buffer.size(); ++c) {
mix_buffer_.resize(buffer.size() / 2); mix_buffer_[(c << 1) + 0] = mix_buffer_[(c << 1) + 1] = buffer[c];
for(size_t c = 0; c < mix_buffer_.size(); ++c) {
mix_buffer_[c] = (buffer[(c << 1) + 0] + buffer[(c << 1) + 1]) >> 1;
// TODO: is there an Accelerate framework solution to this?
}
} else {
// Double up.
mix_buffer_.resize(buffer.size() * 2);
for(size_t c = 0; c < buffer.size(); ++c) {
mix_buffer_[(c << 1) + 0] = mix_buffer_[(c << 1) + 1] = buffer[c];
}
} }
delegate->speaker_did_complete_samples(this, mix_buffer_);
} }
std::atomic<Delegate *> delegate_{nullptr}; delegate->speaker_did_complete_samples(this, mix_buffer_);
}
std::atomic<Delegate *> delegate_{nullptr};
private: private:
void compute_output_rate() { void compute_output_rate() {
// The input rate multiplier is actually used as an output rate divider, // The input rate multiplier is actually used as an output rate divider,
// to confirm to the public interface of a generic speaker being output-centric. // to confirm to the public interface of a generic speaker being output-centric.
set_computed_output_rate(output_cycles_per_second_ / input_rate_multiplier_, output_buffer_size_, stereo_output_); set_computed_output_rate(output_cycles_per_second_ / input_rate_multiplier_, output_buffer_size_, stereo_output_);
} }
int completed_sample_sets_ = 0; int completed_sample_sets_ = 0;
float input_rate_multiplier_ = 1.0f; float input_rate_multiplier_ = 1.0f;
float output_cycles_per_second_ = 1.0f; float output_cycles_per_second_ = 1.0f;
int output_buffer_size_ = 1; int output_buffer_size_ = 1;
std::atomic<bool> stereo_output_{false}; std::atomic<bool> stereo_output_{false};
std::vector<int16_t> mix_buffer_; std::vector<int16_t> mix_buffer_;
}; };
} }

View File

@@ -142,28 +142,28 @@ class ProcessorBase: public ProcessorStorage {
can produce a minor runtime performance improvement. can produce a minor runtime performance improvement.
*/ */
template <Personality personality, typename BusHandler, bool uses_ready_line> class Processor: public ProcessorBase { template <Personality personality, typename BusHandler, bool uses_ready_line> class Processor: public ProcessorBase {
public: public:
/*! /*!
Constructs an instance of the 6502 that will use @c bus_handler for all bus communications. Constructs an instance of the 6502 that will use @c bus_handler for all bus communications.
*/ */
Processor(BusHandler &bus_handler) : ProcessorBase(personality), bus_handler_(bus_handler) {} Processor(BusHandler &bus_handler) : ProcessorBase(personality), bus_handler_(bus_handler) {}
/*! /*!
Runs the 6502 for a supplied number of cycles. Runs the 6502 for a supplied number of cycles.
@param cycles The number of cycles to run the 6502 for. @param cycles The number of cycles to run the 6502 for.
*/ */
void run_for(const Cycles cycles); void run_for(const Cycles cycles);
/*! /*!
Sets the current level of the RDY line. Sets the current level of the RDY line.
@param active @c true if the line is logically active; @c false otherwise. @param active @c true if the line is logically active; @c false otherwise.
*/ */
void set_ready_line(bool active); void set_ready_line(bool active);
private: private:
BusHandler &bus_handler_; BusHandler &bus_handler_;
}; };
#include "Implementation/6502Implementation.hpp" #include "Implementation/6502Implementation.hpp"

View File

@@ -28,131 +28,131 @@ using Type = CPU::MOS6502Esque::Type;
template <Type type, bool has_cias> class ConcreteAllRAMProcessor: template <Type type, bool has_cias> class ConcreteAllRAMProcessor:
public AllRAMProcessor, public CPU::MOS6502Esque::BusHandlerT<type> public AllRAMProcessor, public CPU::MOS6502Esque::BusHandlerT<type>
{ {
public: public:
using typename CPU::MOS6502Esque::BusHandlerT<type>::AddressType; using typename CPU::MOS6502Esque::BusHandlerT<type>::AddressType;
ConcreteAllRAMProcessor(size_t memory_size) : ConcreteAllRAMProcessor(size_t memory_size) :
AllRAMProcessor(memory_size), AllRAMProcessor(memory_size),
mos6502_(*this), mos6502_(*this),
cia1_(cia1_handler_), cia1_(cia1_handler_),
cia2_(cia2_handler_) { cia2_(cia2_handler_) {
mos6502_.set_power_on(false); mos6502_.set_power_on(false);
}
Cycles perform_bus_operation(BusOperation operation, AddressType address, uint8_t *value) {
timestamp_ += Cycles(1);
if constexpr (has_cias) {
cia1_.run_for(HalfCycles(2));
cia2_.run_for(HalfCycles(2));
} }
Cycles perform_bus_operation(BusOperation operation, AddressType address, uint8_t *value) { if(isAccessOperation(operation)) {
timestamp_ += Cycles(1); if(operation == BusOperation::ReadOpcode) {
if constexpr (LogProgramCounter) {
if constexpr (has_cias) { printf("[%04x] %02x a:%04x x:%04x y:%04x p:%02x s:%02x\n", address, memory_[address],
cia1_.run_for(HalfCycles(2)); mos6502_.value_of(Register::A),
cia2_.run_for(HalfCycles(2)); mos6502_.value_of(Register::X),
mos6502_.value_of(Register::Y),
mos6502_.value_of(Register::Flags) & 0xff,
mos6502_.value_of(Register::StackPointer) & 0xff);
}
check_address_for_trap(address);
--instructions_;
} }
if(isAccessOperation(operation)) { if(isReadOperation(operation)) {
if(operation == BusOperation::ReadOpcode) { *value = memory_[address];
if constexpr (LogProgramCounter) {
printf("[%04x] %02x a:%04x x:%04x y:%04x p:%02x s:%02x\n", address, memory_[address],
mos6502_.value_of(Register::A),
mos6502_.value_of(Register::X),
mos6502_.value_of(Register::Y),
mos6502_.value_of(Register::Flags) & 0xff,
mos6502_.value_of(Register::StackPointer) & 0xff);
}
check_address_for_trap(address);
--instructions_;
}
if(isReadOperation(operation)) { if constexpr (has_cias) {
*value = memory_[address]; if((address & 0xff00) == 0xdc00) {
*value = cia1_.read(address);
if constexpr (has_cias) { if constexpr (LogCIAAccesses) {
if((address & 0xff00) == 0xdc00) { printf("[%d] CIA1: %04x -> %02x\n", timestamp_.as<int>(), address, *value);
*value = cia1_.read(address); }
if constexpr (LogCIAAccesses) { } else if((address & 0xff00) == 0xdd00) {
printf("[%d] CIA1: %04x -> %02x\n", timestamp_.as<int>(), address, *value); *value = cia2_.read(address);
} if constexpr (LogCIAAccesses) {
} else if((address & 0xff00) == 0xdd00) { printf("[%d] CIA2: %04x -> %02x\n", timestamp_.as<int>(), address, *value);
*value = cia2_.read(address);
if constexpr (LogCIAAccesses) {
printf("[%d] CIA2: %04x -> %02x\n", timestamp_.as<int>(), address, *value);
}
} }
} }
}
if constexpr (LogAllReads) { if constexpr (LogAllReads) {
// if((address&0xff00) == 0x100) { // if((address&0xff00) == 0x100) {
printf("%04x -> %02x\n", address, *value); printf("%04x -> %02x\n", address, *value);
// } // }
} }
} else { } else {
memory_[address] = *value; memory_[address] = *value;
if constexpr (has_cias) { if constexpr (has_cias) {
if((address & 0xff00) == 0xdc00) { if((address & 0xff00) == 0xdc00) {
cia1_.write(address, *value); cia1_.write(address, *value);
if constexpr (LogCIAAccesses) { if constexpr (LogCIAAccesses) {
printf("[%d] CIA1: %04x <- %02x\n", timestamp_.as<int>(), address, *value); printf("[%d] CIA1: %04x <- %02x\n", timestamp_.as<int>(), address, *value);
} }
} else if((address & 0xff00) == 0xdd00) { } else if((address & 0xff00) == 0xdd00) {
cia2_.write(address, *value); cia2_.write(address, *value);
if constexpr (LogCIAAccesses) { if constexpr (LogCIAAccesses) {
printf("[%d] CIA2: %04x <- %02x\n", timestamp_.as<int>(), address, *value); printf("[%d] CIA2: %04x <- %02x\n", timestamp_.as<int>(), address, *value);
}
} }
} }
}
if constexpr (LogAllWrites) { if constexpr (LogAllWrites) {
// if((address&0xff00) == 0x100) { // if((address&0xff00) == 0x100) {
printf("%04x <- %02x\n", address, *value); printf("%04x <- %02x\n", address, *value);
// } // }
}
} }
} }
mos6502_.set_irq_line(cia1_.get_interrupt_line());
mos6502_.set_nmi_line(cia2_.get_interrupt_line());
return Cycles(1);
} }
void run_for(const Cycles cycles) { mos6502_.set_irq_line(cia1_.get_interrupt_line());
mos6502_.run_for(cycles); mos6502_.set_nmi_line(cia2_.get_interrupt_line());
return Cycles(1);
}
void run_for(const Cycles cycles) {
mos6502_.run_for(cycles);
}
void run_for_instructions(int count) {
instructions_ = count;
while(instructions_) {
mos6502_.run_for(Cycles(1));
} }
}
void run_for_instructions(int count) { bool is_jammed() {
instructions_ = count; return mos6502_.is_jammed();
while(instructions_) { }
mos6502_.run_for(Cycles(1));
}
}
bool is_jammed() { void set_irq_line(bool value) {
return mos6502_.is_jammed(); mos6502_.set_irq_line(value);
} }
void set_irq_line(bool value) { void set_nmi_line(bool value) {
mos6502_.set_irq_line(value); mos6502_.set_nmi_line(value);
} }
void set_nmi_line(bool value) { uint16_t value_of(Register r) {
mos6502_.set_nmi_line(value); return mos6502_.value_of(r);
} }
uint16_t value_of(Register r) { void set_value_of(Register r, uint16_t value) {
return mos6502_.value_of(r); mos6502_.set_value_of(r, value);
} }
void set_value_of(Register r, uint16_t value) { private:
mos6502_.set_value_of(r, value); CPU::MOS6502Esque::Processor<type, ConcreteAllRAMProcessor, false> mos6502_;
} int instructions_ = 0;
private: class PortHandler: public MOS::MOS6526::PortHandler {};
CPU::MOS6502Esque::Processor<type, ConcreteAllRAMProcessor, false> mos6502_; PortHandler cia1_handler_, cia2_handler_;
int instructions_ = 0;
class PortHandler: public MOS::MOS6526::PortHandler {}; MOS::MOS6526::MOS6526<PortHandler, MOS::MOS6526::Personality::P6526> cia1_, cia2_;
PortHandler cia1_handler_, cia2_handler_;
MOS::MOS6526::MOS6526<PortHandler, MOS::MOS6526::Personality::P6526> cia1_, cia2_;
}; };
} }

View File

@@ -34,58 +34,58 @@ enum ExtendedBusOutput {
#include "Implementation/65816Storage.hpp" #include "Implementation/65816Storage.hpp"
class ProcessorBase: protected ProcessorStorage { class ProcessorBase: protected ProcessorStorage {
public: public:
inline void set_power_on(bool); inline void set_power_on(bool);
inline void set_irq_line(bool); inline void set_irq_line(bool);
inline void set_nmi_line(bool); inline void set_nmi_line(bool);
inline void set_reset_line(bool); inline void set_reset_line(bool);
inline void set_abort_line(bool); inline void set_abort_line(bool);
inline bool get_is_resetting() const; inline bool get_is_resetting() const;
/*! /*!
Returns the current state of all lines not ordinarily pushed to the BusHandler, Returns the current state of all lines not ordinarily pushed to the BusHandler,
as listed in the ExtendedBusOutput enum. as listed in the ExtendedBusOutput enum.
*/ */
inline int get_extended_bus_output(); inline int get_extended_bus_output();
/*! /*!
Provided for symmetry with the 6502; a 65816 is never jammed. Provided for symmetry with the 6502; a 65816 is never jammed.
*/ */
inline bool is_jammed() const; inline bool is_jammed() const;
/*! /*!
FOR TESTING PURPOSES ONLY: forces the processor into a state where FOR TESTING PURPOSES ONLY: forces the processor into a state where
the next thing it intends to do is fetch a new opcode. the next thing it intends to do is fetch a new opcode.
*/ */
inline void restart_operation_fetch(); inline void restart_operation_fetch();
void set_value_of(Register r, uint16_t value); void set_value_of(Register r, uint16_t value);
uint16_t value_of(Register r) const; uint16_t value_of(Register r) const;
}; };
template <typename BusHandler, bool uses_ready_line> class Processor: public ProcessorBase { template <typename BusHandler, bool uses_ready_line> class Processor: public ProcessorBase {
public: public:
/*! /*!
Constructs an instance of the 6502 that will use @c bus_handler for all bus communications. Constructs an instance of the 6502 that will use @c bus_handler for all bus communications.
*/ */
Processor(BusHandler &bus_handler) : bus_handler_(bus_handler) {} Processor(BusHandler &bus_handler) : bus_handler_(bus_handler) {}
/*! /*!
Runs the 6502 for a supplied number of cycles. Runs the 6502 for a supplied number of cycles.
@param cycles The number of cycles to run the 6502 for. @param cycles The number of cycles to run the 6502 for.
*/ */
void run_for(const Cycles cycles); void run_for(const Cycles);
/*! /*!
Sets the current level of the RDY line. Sets the current level of the RDY line.
@param active @c true if the line is logically active; @c false otherwise. @param active @c true if the line is logically active; @c false otherwise.
*/ */
void set_ready_line(bool active); void set_ready_line(bool active);
private: private:
BusHandler &bus_handler_; BusHandler &bus_handler_;
}; };
#include "Implementation/65816Implementation.hpp" #include "Implementation/65816Implementation.hpp"

View File

@@ -108,7 +108,7 @@ struct CPU::WDC65816::ProcessorStorageConstructor {
storage_.micro_ops_.push_back(OperationDecode); storage_.micro_ops_.push_back(OperationDecode);
} }
private: private:
PatternTable::iterator install(Generator generator, AccessType access_type = AccessType::Read) { PatternTable::iterator install(Generator generator, AccessType access_type = AccessType::Read) {
// Check whether this access type + addressing mode generator has already been generated. // Check whether this access type + addressing mode generator has already been generated.

View File

@@ -395,15 +395,15 @@ struct ProcessorStorage {
return reinterpret_cast<uint8_t *>(&value); return reinterpret_cast<uint8_t *>(&value);
} }
private: private:
uint8_t *byte(int pointer) { uint8_t *byte(int pointer) {
assert(pointer >= 0 && pointer < 4); assert(pointer >= 0 && pointer < 4);
#if TARGET_RT_BIG_ENDIAN #if TARGET_RT_BIG_ENDIAN
return reinterpret_cast<uint8_t *>(&value) + (3 ^ pointer); return reinterpret_cast<uint8_t *>(&value) + (3 ^ pointer);
#else #else
return reinterpret_cast<uint8_t *>(&value) + pointer; return reinterpret_cast<uint8_t *>(&value) + pointer;
#endif #endif
} }
}; };
Buffer instruction_buffer_, data_buffer_; Buffer instruction_buffer_, data_buffer_;
uint32_t data_address_; uint32_t data_address_;

View File

@@ -438,61 +438,61 @@ namespace CPU::MC68000 {
*/ */
template <class BusHandler, bool dtack_is_implicit = true, bool permit_overrun = true, bool signal_will_perform = false> template <class BusHandler, bool dtack_is_implicit = true, bool permit_overrun = true, bool signal_will_perform = false>
class Processor: private ProcessorBase { class Processor: private ProcessorBase {
public: public:
Processor(BusHandler &bus_handler) : ProcessorBase(), bus_handler_(bus_handler) {} Processor(BusHandler &bus_handler) : ProcessorBase(), bus_handler_(bus_handler) {}
Processor(const Processor& rhs) = delete; Processor(const Processor& rhs) = delete;
Processor& operator=(const Processor& rhs) = delete; Processor& operator=(const Processor& rhs) = delete;
void run_for(HalfCycles duration); void run_for(HalfCycles duration);
/// @returns The current processor state. /// @returns The current processor state.
CPU::MC68000::State get_state(); CPU::MC68000::State get_state();
/// Sets the current processor state. /// Sets the current processor state.
void set_state(const CPU::MC68000::State &); void set_state(const CPU::MC68000::State &);
/// Sets all registers to the values provided, fills the prefetch queue and ensures the /// Sets all registers to the values provided, fills the prefetch queue and ensures the
/// next action the processor will take is to decode whatever is in the queue. /// next action the processor will take is to decode whatever is in the queue.
/// ///
/// The queue is filled synchronously, during this call, causing calls to the bus handler. /// The queue is filled synchronously, during this call, causing calls to the bus handler.
void decode_from_state(const InstructionSet::M68k::RegisterSet &); void decode_from_state(const InstructionSet::M68k::RegisterSet &);
// TODO: bus ack/grant, halt, // TODO: bus ack/grant, halt,
/// Sets the DTack line — @c true for active, @c false for inactive. /// Sets the DTack line — @c true for active, @c false for inactive.
inline void set_dtack(bool dtack) { inline void set_dtack(bool dtack) {
dtack_ = dtack; dtack_ = dtack;
} }
/// Sets the VPA (valid peripheral address) line — @c true for active, @c false for inactive. /// Sets the VPA (valid peripheral address) line — @c true for active, @c false for inactive.
inline void set_is_peripheral_address(bool is_peripheral_address) { inline void set_is_peripheral_address(bool is_peripheral_address) {
vpa_ = is_peripheral_address; vpa_ = is_peripheral_address;
} }
/// Sets the bus error line — @c true for active, @c false for inactive. /// Sets the bus error line — @c true for active, @c false for inactive.
inline void set_bus_error(bool bus_error) { inline void set_bus_error(bool bus_error) {
berr_ = bus_error; berr_ = bus_error;
} }
/// Sets the interrupt lines, IPL0, IPL1 and IPL2. /// Sets the interrupt lines, IPL0, IPL1 and IPL2.
inline void set_interrupt_level(int interrupt_level) { inline void set_interrupt_level(int interrupt_level) {
bus_interrupt_level_ = interrupt_level; bus_interrupt_level_ = interrupt_level;
} }
/// @returns The current phase of the E clock; this will be a number of /// @returns The current phase of the E clock; this will be a number of
/// half-cycles between 0 and 19 inclusive, indicating how far the 68000 /// half-cycles between 0 and 19 inclusive, indicating how far the 68000
/// is into the current E cycle. /// is into the current E cycle.
/// ///
/// This is guaranteed to be 0 at initial 68000 construction. It is not guaranteed /// This is guaranteed to be 0 at initial 68000 construction. It is not guaranteed
/// to return the correct result if called during a bus transaction. /// to return the correct result if called during a bus transaction.
HalfCycles get_e_clock_phase() { HalfCycles get_e_clock_phase() {
return e_clock_phase_; return e_clock_phase_;
} }
void reset(); void reset();
private: private:
BusHandler &bus_handler_; BusHandler &bus_handler_;
}; };
} }

View File

@@ -17,32 +17,32 @@
namespace CPU { namespace CPU {
class AllRAMProcessor { class AllRAMProcessor {
public: public:
AllRAMProcessor(std::size_t memory_size); AllRAMProcessor(std::size_t memory_size);
HalfCycles get_timestamp(); HalfCycles get_timestamp();
void set_data_at_address(size_t startAddress, size_t length, const uint8_t *data); void set_data_at_address(size_t startAddress, size_t length, const uint8_t *data);
void get_data_at_address(size_t startAddress, size_t length, uint8_t *data); void get_data_at_address(size_t startAddress, size_t length, uint8_t *data);
class TrapHandler { class TrapHandler {
public: public:
virtual void processor_did_trap(AllRAMProcessor &, uint16_t address) = 0; virtual void processor_did_trap(AllRAMProcessor &, uint16_t address) = 0;
}; };
void set_trap_handler(TrapHandler *trap_handler); void set_trap_handler(TrapHandler *trap_handler);
void add_trap_address(uint16_t address); void add_trap_address(uint16_t address);
protected: protected:
std::vector<uint8_t> memory_; std::vector<uint8_t> memory_;
HalfCycles timestamp_; HalfCycles timestamp_;
inline void check_address_for_trap(uint16_t address) { inline void check_address_for_trap(uint16_t address) {
if(traps_[address]) { if(traps_[address]) {
trap_handler_->processor_did_trap(*this, address); trap_handler_->processor_did_trap(*this, address);
}
} }
}
private: private:
TrapHandler *trap_handler_; TrapHandler *trap_handler_;
std::vector<bool> traps_; std::vector<bool> traps_;
}; };
} }

View File

@@ -376,7 +376,7 @@ struct PartialMachineCycle {
} }
PartialMachineCycle(const PartialMachineCycle &rhs) noexcept; PartialMachineCycle(const PartialMachineCycle &rhs) noexcept;
PartialMachineCycle(Operation operation, HalfCycles length, uint16_t *address, uint8_t *value, bool was_requested) noexcept; PartialMachineCycle(Operation, HalfCycles, uint16_t *address, uint8_t *value, bool was_requested) noexcept;
PartialMachineCycle() noexcept; PartialMachineCycle() noexcept;
}; };
@@ -386,18 +386,18 @@ struct PartialMachineCycle {
handler. handler.
*/ */
class BusHandler { class BusHandler {
public: public:
/*! /*!
Announces that the Z80 has performed the partial machine cycle defined by @c cycle. Announces that the Z80 has performed the partial machine cycle defined by @c cycle.
@returns The number of additional HalfCycles that passed in objective time while this Z80 operation was ongoing. @returns The number of additional HalfCycles that passed in objective time while this Z80 operation was ongoing.
On an archetypal machine this will be HalfCycles(0) but some architectures may choose not to clock the Z80 On an archetypal machine this will be HalfCycles(0) but some architectures may choose not to clock the Z80
during some periods or may impose wait states so predictably that it's more efficient just to add them during some periods or may impose wait states so predictably that it's more efficient just to add them
via this mechanism. via this mechanism.
*/ */
HalfCycles perform_machine_cycle([[maybe_unused]] const PartialMachineCycle &cycle) { HalfCycles perform_machine_cycle([[maybe_unused]] const PartialMachineCycle &) {
return HalfCycles(0); return HalfCycles(0);
} }
}; };
#include "Implementation/Z80Storage.hpp" #include "Implementation/Z80Storage.hpp"
@@ -406,82 +406,82 @@ class BusHandler {
A base class from which the Z80 descends; separated for implementation reasons only. A base class from which the Z80 descends; separated for implementation reasons only.
*/ */
class ProcessorBase: public ProcessorStorage { class ProcessorBase: public ProcessorStorage {
public: public:
/*! /*!
Gets the value of a register. Gets the value of a register.
@see set_value_of @see set_value_of
@param r The register to set. @param r The register to set.
@returns The value of the register. 8-bit registers will be returned as unsigned. @returns The value of the register. 8-bit registers will be returned as unsigned.
*/ */
uint16_t value_of(Register r) const; uint16_t value_of(Register r) const;
/*! /*!
Sets the value of a register. Sets the value of a register.
@see value_of @see value_of
@param r The register to set. @param r The register to set.
@param value The value to set. If the register is only 8 bit, the value will be truncated. @param value The value to set. If the register is only 8 bit, the value will be truncated.
*/ */
void set_value_of(Register r, uint16_t value); void set_value_of(Register r, uint16_t value);
/*! /*!
Gets the value of the HALT output line. Gets the value of the HALT output line.
*/ */
inline bool get_halt_line() const; inline bool get_halt_line() const;
/*! /*!
Sets the logical value of the interrupt line. Sets the logical value of the interrupt line.
@param offset If called while within perform_machine_cycle this may be a value indicating @param offset If called while within perform_machine_cycle this may be a value indicating
how many cycles before now the line changed state. The value may not be longer than the how many cycles before now the line changed state. The value may not be longer than the
current machine cycle. If called at any other time, this must be zero. current machine cycle. If called at any other time, this must be zero.
*/ */
inline void set_interrupt_line(bool value, HalfCycles offset = 0); inline void set_interrupt_line(bool, HalfCycles offset = 0);
/*! /*!
Gets the value of the interrupt line. Gets the value of the interrupt line.
*/ */
inline bool get_interrupt_line() const; inline bool get_interrupt_line() const;
/*! /*!
Sets the logical value of the non-maskable interrupt line. Sets the logical value of the non-maskable interrupt line.
@param offset See discussion in set_interrupt_line. @param offset See discussion in set_interrupt_line.
*/ */
inline void set_non_maskable_interrupt_line(bool value, HalfCycles offset = 0); inline void set_non_maskable_interrupt_line(bool, HalfCycles offset = 0);
/*! /*!
Gets the value of the non-maskable interrupt line. Gets the value of the non-maskable interrupt line.
*/ */
inline bool get_non_maskable_interrupt_line() const; inline bool get_non_maskable_interrupt_line() const;
/*! /*!
Sets the logical value of the reset line. Sets the logical value of the reset line.
*/ */
inline void set_reset_line(bool value); inline void set_reset_line(bool);
/*! /*!
Gets whether the Z80 would reset at the next opportunity. Gets whether the Z80 would reset at the next opportunity.
@returns @c true if the line is logically active; @c false otherwise. @returns @c true if the line is logically active; @c false otherwise.
*/ */
bool get_is_resetting() const; bool get_is_resetting() const;
/*! /*!
This emulation automatically sets itself up in power-on state at creation, which has the effect of triggering a This emulation automatically sets itself up in power-on state at creation, which has the effect of triggering a
reset at the first opportunity. Use @c reset_power_on to disable that behaviour. reset at the first opportunity. Use @c reset_power_on to disable that behaviour.
*/ */
void reset_power_on(); void reset_power_on();
/*! /*!
@returns @c true if the Z80 is currently beginning to fetch a new instruction; @c false otherwise. @returns @c true if the Z80 is currently beginning to fetch a new instruction; @c false otherwise.
This is not a speedy operation. This is not a speedy operation.
*/ */
bool is_starting_new_instruction() const; bool is_starting_new_instruction() const;
}; };
/*! /*!
@@ -493,45 +493,45 @@ class ProcessorBase: public ProcessorStorage {
support either can produce a minor runtime performance improvement. support either can produce a minor runtime performance improvement.
*/ */
template <class T, bool uses_bus_request, bool uses_wait_line> class Processor: public ProcessorBase { template <class T, bool uses_bus_request, bool uses_wait_line> class Processor: public ProcessorBase {
public: public:
Processor(T &bus_handler); Processor(T &bus_handler);
/*! /*!
Runs the Z80 for a supplied number of cycles. Runs the Z80 for a supplied number of cycles.
@discussion Subclasses must implement @c perform_machine_cycle(const PartialMachineCycle &cycle) . @discussion Subclasses must implement @c perform_machine_cycle(const PartialMachineCycle &cycle) .
If it is a read operation then @c value will be seeded with the value 0xff. If it is a read operation then @c value will be seeded with the value 0xff.
@param cycles The number of cycles to run for. @param cycles The number of cycles to run for.
*/ */
void run_for(const HalfCycles cycles); void run_for(const HalfCycles);
/*! /*!
Sets the logical value of the bus request line, having asserted that this Z80 supports the bus request line. Sets the logical value of the bus request line, having asserted that this Z80 supports the bus request line.
*/ */
void set_bus_request_line(bool value); void set_bus_request_line(bool);
/*! /*!
Gets the logical value of the bus request line. Gets the logical value of the bus request line.
*/ */
bool get_bus_request_line() const; bool get_bus_request_line() const;
/*! /*!
Sets the logical value of the wait line, having asserted that this Z80 supports the wait line. Sets the logical value of the wait line, having asserted that this Z80 supports the wait line.
*/ */
void set_wait_line(bool value); void set_wait_line(bool);
/*! /*!
Gets the logical value of the bus request line. Gets the logical value of the bus request line.
*/ */
bool get_wait_line() const; bool get_wait_line() const;
private: private:
T &bus_handler_; T &bus_handler_;
void assemble_page(InstructionPage &target, InstructionTable &table, bool add_offsets); void assemble_page(InstructionPage &, InstructionTable &, bool add_offsets);
void copy_program(const MicroOp *source, std::vector<MicroOp> &destination); void copy_program(const MicroOp *source, std::vector<MicroOp> &destination);
}; };
#include "Implementation/Z80Implementation.hpp" #include "Implementation/Z80Implementation.hpp"