mirror of
https://github.com/TomHarte/CLK.git
synced 2024-12-13 00:29:14 +00:00
Merge pull request #1428 from TomHarte/MoreIndentation
Adjust more dangling indentation changes.
This commit is contained in:
commit
88b31ea940
@ -70,7 +70,7 @@ private:
|
||||
uint8_t track_;
|
||||
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;
|
||||
bit_count_++;
|
||||
}
|
||||
@ -107,7 +107,7 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void process_index_hole() {
|
||||
void process_index_hole() override {
|
||||
index_count_++;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
template <typename Machine> class AuxiliaryMemorySwitches {
|
||||
public:
|
||||
static constexpr bool Auxiliary = true;
|
||||
static constexpr bool Main = false;
|
||||
static constexpr bool ROM = true;
|
||||
static constexpr bool Card = false;
|
||||
public:
|
||||
static constexpr bool Auxiliary = true;
|
||||
static constexpr bool Main = false;
|
||||
static constexpr bool ROM = true;
|
||||
static constexpr bool Card = false;
|
||||
|
||||
/// Describes banking state between $0200 and $BFFF.
|
||||
struct MainState {
|
||||
struct Region {
|
||||
/// @c true indicates auxiliary memory should be read from; @c false indicates main.
|
||||
bool read = false;
|
||||
/// @c true indicates auxiliary memory should be written to; @c false indicates main.
|
||||
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 $0200 and $BFFF.
|
||||
struct MainState {
|
||||
struct Region {
|
||||
/// @c true indicates auxiliary memory should be read from; @c false indicates main.
|
||||
bool read = false;
|
||||
/// @c true indicates auxiliary memory should be written to; @c false indicates main.
|
||||
bool write = false;
|
||||
};
|
||||
|
||||
/// Describes banking state between $C100 and $Cfff.
|
||||
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;
|
||||
/// 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 CardState &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 {
|
||||
bool operator != (const MainState &rhs) 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);
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const MainState &main_state() const {
|
||||
return main_state_;
|
||||
}
|
||||
/// Describes banking state between $C100 and $Cfff.
|
||||
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 {
|
||||
return card_state_;
|
||||
bool operator != (const CardState &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;
|
||||
}
|
||||
};
|
||||
|
||||
/// @returns @c true if the alternative zero page should be used; @c false otherwise.
|
||||
ZeroState zero_state() const {
|
||||
return switches_.alternative_zero_page;
|
||||
}
|
||||
/// Descibes banking state between $0000 and $01ff; @c true indicates that auxiliary memory should be used; @c false indicates main memory.
|
||||
using ZeroState = bool;
|
||||
|
||||
const SwitchState switches() const {
|
||||
return switches_;
|
||||
}
|
||||
/// 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() {
|
||||
switches_.reset();
|
||||
*this = SwitchState();
|
||||
}
|
||||
};
|
||||
|
||||
set_main_paging();
|
||||
set_zero_page_paging();
|
||||
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;
|
||||
}
|
||||
|
||||
private:
|
||||
Machine &machine_;
|
||||
SwitchState switches_;
|
||||
if(address < 0xc000 || address >= 0xc058) return;
|
||||
|
||||
MainState main_state_;
|
||||
void set_main_paging() {
|
||||
const auto previous_state = main_state_;
|
||||
switch(address) {
|
||||
default: break;
|
||||
|
||||
// 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 {
|
||||
main_state_.region_20_40 = main_state_.base;
|
||||
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
|
||||
(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 {
|
||||
main_state_.region_04_08 = main_state_.region_20_40 = main_state_.base;
|
||||
}
|
||||
|
||||
if(previous_state != main_state_) {
|
||||
machine_.template set_paging<PagingType::Main>();
|
||||
main_state_.region_20_40 = main_state_.base;
|
||||
}
|
||||
} else {
|
||||
main_state_.region_04_08 = main_state_.region_20_40 = main_state_.base;
|
||||
}
|
||||
|
||||
CardState card_state_;
|
||||
void set_card_paging() {
|
||||
const auto previous_state = card_state_;
|
||||
if(previous_state != main_state_) {
|
||||
machine_.template set_paging<PagingType::Main>();
|
||||
}
|
||||
}
|
||||
|
||||
// By default apply the CX switch through to $C7FF.
|
||||
card_state_.region_C1_C3 = card_state_.region_C4_C8 = switches_.internal_CX_rom;
|
||||
CardState card_state_;
|
||||
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
|
||||
// first half of the CX region is diabled, if its specific switch is also disabled.
|
||||
if(!switches_.internal_CX_rom && !switches_.slot_C3_rom) {
|
||||
card_state_.region_C3 = true;
|
||||
} else {
|
||||
card_state_.region_C3 = card_state_.region_C1_C3;
|
||||
}
|
||||
// By default apply the CX switch through to $C7FF.
|
||||
card_state_.region_C1_C3 = card_state_.region_C4_C8 = switches_.internal_CX_rom;
|
||||
|
||||
// Apply the CX switch to $C800+, but also allow the C8 switch to select that region in isolation.
|
||||
card_state_.region_C8_D0 = switches_.internal_CX_rom || switches_.internal_C8_rom;
|
||||
|
||||
if(previous_state != card_state_) {
|
||||
machine_.template set_paging<PagingType::CardArea>();
|
||||
}
|
||||
// Allow the C3 region to be switched to internal ROM in isolation even if the rest of the
|
||||
// first half of the CX region is diabled, if its specific switch is also disabled.
|
||||
if(!switches_.internal_CX_rom && !switches_.slot_C3_rom) {
|
||||
card_state_.region_C3 = true;
|
||||
} else {
|
||||
card_state_.region_C3 = card_state_.region_C1_C3;
|
||||
}
|
||||
|
||||
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>();
|
||||
// Apply the CX switch to $C800+, but also allow the C8 switch to select that region in isolation.
|
||||
card_state_.region_C8_D0 = switches_.internal_CX_rom || switches_.internal_C8_rom;
|
||||
|
||||
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>();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -19,67 +19,67 @@
|
||||
namespace Apple::IIgs::ADB {
|
||||
|
||||
class GLU: public InstructionSet::M50740::PortHandler {
|
||||
public:
|
||||
GLU();
|
||||
public:
|
||||
GLU();
|
||||
|
||||
uint8_t get_keyboard_data();
|
||||
uint8_t get_mouse_data();
|
||||
uint8_t get_modifier_status();
|
||||
uint8_t get_any_key_down();
|
||||
uint8_t get_keyboard_data();
|
||||
uint8_t get_mouse_data();
|
||||
uint8_t get_modifier_status();
|
||||
uint8_t get_any_key_down();
|
||||
|
||||
uint8_t get_data();
|
||||
uint8_t get_status();
|
||||
uint8_t get_data();
|
||||
uint8_t get_status();
|
||||
|
||||
void set_command(uint8_t);
|
||||
void set_status(uint8_t);
|
||||
void clear_key_strobe();
|
||||
void set_command(uint8_t);
|
||||
void set_status(uint8_t);
|
||||
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_option_button() const;
|
||||
bool get_command_button() const;
|
||||
bool get_option_button() const;
|
||||
|
||||
void set_vertical_blank(bool);
|
||||
bool get_vertical_blank() {
|
||||
return vertical_blank_;
|
||||
}
|
||||
void set_vertical_blank(bool);
|
||||
bool get_vertical_blank() {
|
||||
return vertical_blank_;
|
||||
}
|
||||
|
||||
Apple::ADB::Keyboard &keyboard() {
|
||||
return keyboard_;
|
||||
}
|
||||
Inputs::Mouse &get_mouse() {
|
||||
return mouse_;
|
||||
}
|
||||
Apple::ADB::Keyboard &keyboard() {
|
||||
return keyboard_;
|
||||
}
|
||||
Inputs::Mouse &get_mouse() {
|
||||
return mouse_;
|
||||
}
|
||||
|
||||
private:
|
||||
InstructionSet::M50740::Executor executor_;
|
||||
private:
|
||||
InstructionSet::M50740::Executor executor_;
|
||||
|
||||
void run_ports_for(Cycles) override;
|
||||
void set_port_output(int port, uint8_t value) override;
|
||||
uint8_t get_port_input(int port) override;
|
||||
void run_ports_for(Cycles) override;
|
||||
void set_port_output(int port, uint8_t value) override;
|
||||
uint8_t get_port_input(int port) override;
|
||||
|
||||
uint8_t registers_[16]{};
|
||||
uint8_t registers_[16]{};
|
||||
|
||||
uint8_t register_address_;
|
||||
uint8_t register_latch_ = 0xff;
|
||||
bool register_strobe_ = false;
|
||||
uint8_t register_address_;
|
||||
uint8_t register_latch_ = 0xff;
|
||||
bool register_strobe_ = false;
|
||||
|
||||
uint8_t status_ = 0x00;
|
||||
uint8_t status_ = 0x00;
|
||||
|
||||
Apple::ADB::Bus bus_;
|
||||
size_t controller_id_;
|
||||
Apple::ADB::Bus bus_;
|
||||
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.
|
||||
Apple::ADB::Mouse mouse_;
|
||||
Apple::ADB::Keyboard keyboard_;
|
||||
// For now, attach only a keyboard and mouse.
|
||||
Apple::ADB::Mouse mouse_;
|
||||
Apple::ADB::Keyboard keyboard_;
|
||||
};
|
||||
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -20,144 +20,144 @@
|
||||
namespace Apple::IIgs {
|
||||
|
||||
class MemoryMap {
|
||||
public:
|
||||
// MARK: - Initial construction and configuration.
|
||||
public:
|
||||
// MARK: - Initial construction and configuration.
|
||||
|
||||
MemoryMap(bool is_rom03) : auxiliary_switches_(*this), language_card_(*this) {
|
||||
setup_shadow_maps(is_rom03);
|
||||
}
|
||||
MemoryMap(bool is_rom03) : auxiliary_switches_(*this), language_card_(*this) {
|
||||
setup_shadow_maps(is_rom03);
|
||||
}
|
||||
|
||||
/// Sets the ROM and RAM storage underlying this MemoryMap.
|
||||
void set_storage(std::vector<uint8_t> &ram, std::vector<uint8_t> &rom);
|
||||
/// Sets the ROM and RAM storage underlying this MemoryMap.
|
||||
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);
|
||||
uint8_t get_shadow_register() const;
|
||||
void set_shadow_register(uint8_t value);
|
||||
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);
|
||||
uint8_t get_state_register() const;
|
||||
void set_state_register(uint8_t value);
|
||||
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>;
|
||||
const AuxiliaryMemorySwitches &auxiliary_switches() const {
|
||||
return auxiliary_switches_;
|
||||
}
|
||||
using AuxiliaryMemorySwitches = Apple::II::AuxiliaryMemorySwitches<MemoryMap>;
|
||||
const AuxiliaryMemorySwitches &auxiliary_switches() const {
|
||||
return auxiliary_switches_;
|
||||
}
|
||||
|
||||
using LanguageCardSwitches = Apple::II::LanguageCardSwitches<MemoryMap>;
|
||||
const LanguageCardSwitches &language_card_switches() const {
|
||||
return language_card_;
|
||||
}
|
||||
using LanguageCardSwitches = Apple::II::LanguageCardSwitches<MemoryMap>;
|
||||
const LanguageCardSwitches &language_card_switches() const {
|
||||
return language_card_;
|
||||
}
|
||||
|
||||
// MARK: - Accessors for reading and writing RAM.
|
||||
// MARK: - Accessors for reading and writing RAM.
|
||||
|
||||
struct Region {
|
||||
uint8_t *write = nullptr;
|
||||
const uint8_t *read = nullptr;
|
||||
uint8_t flags = 0;
|
||||
struct Region {
|
||||
uint8_t *write = nullptr;
|
||||
const uint8_t *read = nullptr;
|
||||
uint8_t flags = 0;
|
||||
|
||||
enum Flag: uint8_t {
|
||||
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.
|
||||
};
|
||||
enum Flag: uint8_t {
|
||||
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.
|
||||
};
|
||||
};
|
||||
|
||||
const Region ®ion(uint32_t address) const { return regions_[region_map_[address >> 8]]; }
|
||||
uint8_t read(const Region ®ion, uint32_t address) const {
|
||||
return region.read ? region.read[address] : 0xff;
|
||||
const Region ®ion(uint32_t address) const { return regions_[region_map_[address >> 8]]; }
|
||||
uint8_t read(const Region ®ion, uint32_t address) const {
|
||||
return region.read ? region.read[address] : 0xff;
|
||||
}
|
||||
|
||||
bool is_shadowed(const Region ®ion, uint32_t address) const {
|
||||
// ROM is never shadowed.
|
||||
if(!region.write) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_shadowed(const Region ®ion, uint32_t address) const {
|
||||
// ROM is never shadowed.
|
||||
if(!region.write) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto physical = physical_address(region, address);
|
||||
assert(physical <= 0xff'ffff);
|
||||
return shadow_pages_[(physical >> 10) & 127] && shadow_banks_[physical >> 17];
|
||||
}
|
||||
void write(const Region ®ion, 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;
|
||||
const auto physical = physical_address(region, address);
|
||||
assert(physical <= 0xff'ffff);
|
||||
return shadow_pages_[(physical >> 10) & 127] && shadow_banks_[physical >> 17];
|
||||
}
|
||||
void write(const Region ®ion, uint32_t address, uint8_t value) {
|
||||
if(!region.write) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The objective is to support shadowing:
|
||||
// 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.
|
||||
// Write once.
|
||||
region.write[address] = value;
|
||||
|
||||
private:
|
||||
AuxiliaryMemorySwitches auxiliary_switches_;
|
||||
LanguageCardSwitches language_card_;
|
||||
friend AuxiliaryMemorySwitches;
|
||||
friend LanguageCardSwitches;
|
||||
// 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;
|
||||
}
|
||||
|
||||
uint8_t shadow_register_ = 0x00;
|
||||
uint8_t speed_register_ = 0x00;
|
||||
// The objective is to support shadowing:
|
||||
// 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);
|
||||
template <int type> void set_paging();
|
||||
uint8_t shadow_register_ = 0x00;
|
||||
uint8_t speed_register_ = 0x00;
|
||||
|
||||
uint8_t *ram_base_ = nullptr;
|
||||
// MARK: - Banking.
|
||||
|
||||
// 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.
|
||||
void assert_is_region(uint8_t start, uint8_t end);
|
||||
template <int type> void set_paging();
|
||||
|
||||
std::size_t physical_address(const Region ®ion, uint32_t address) const {
|
||||
return std::size_t(®ion.write[address] - ram_base_);
|
||||
}
|
||||
uint8_t *ram_base_ = nullptr;
|
||||
|
||||
// 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
|
||||
// beyond the first 64 bits at the time of writing, alas, so these are generated at runtime.
|
||||
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();
|
||||
std::size_t physical_address(const Region ®ion, uint32_t address) const {
|
||||
return std::size_t(®ion.write[address] - ram_base_);
|
||||
}
|
||||
|
||||
uint8_t *shadow_base_[2] = {nullptr, nullptr};
|
||||
// MARK: - Shadowing
|
||||
|
||||
// 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_{};
|
||||
// Various precomputed bitsets describing key regions; std::bitset doesn't support constexpr instantiation
|
||||
// beyond the first 64 bits at the time of writing, alas, so these are generated at runtime.
|
||||
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
|
||||
// each is a potential source of shadowing.
|
||||
std::bitset<128> shadow_banks_{};
|
||||
uint8_t *shadow_base_[2] = {nullptr, nullptr};
|
||||
|
||||
// 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_{};
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -17,90 +17,90 @@
|
||||
namespace Apple::IIgs::Sound {
|
||||
|
||||
class GLU: public Outputs::Speaker::BufferSource<GLU, false> { // TODO: isn't this stereo?
|
||||
public:
|
||||
GLU(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
public:
|
||||
GLU(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
|
||||
void set_control(uint8_t);
|
||||
uint8_t get_control();
|
||||
void set_data(uint8_t);
|
||||
uint8_t get_data();
|
||||
void set_address_low(uint8_t);
|
||||
uint8_t get_address_low();
|
||||
void set_address_high(uint8_t);
|
||||
uint8_t get_address_high();
|
||||
void set_control(uint8_t);
|
||||
uint8_t get_control();
|
||||
void set_data(uint8_t);
|
||||
uint8_t get_data();
|
||||
void set_address_low(uint8_t);
|
||||
uint8_t get_address_low();
|
||||
void set_address_high(uint8_t);
|
||||
uint8_t get_address_high();
|
||||
|
||||
void run_for(Cycles);
|
||||
Cycles next_sequence_point() const;
|
||||
bool get_interrupt_line();
|
||||
void run_for(Cycles);
|
||||
Cycles next_sequence_point() const;
|
||||
bool get_interrupt_line();
|
||||
|
||||
// SampleSource.
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
bool is_zero_level() const { return false; } // TODO.
|
||||
// SampleSource.
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
bool is_zero_level() const { return false; } // TODO.
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
|
||||
uint16_t address_ = 0;
|
||||
uint16_t address_ = 0;
|
||||
|
||||
// Use a circular buffer for piping memory alterations onto the audio
|
||||
// thread; it would be prohibitive to defer every write individually.
|
||||
//
|
||||
// Assumed: on most modern architectures, an atomic 64-bit read or
|
||||
// write can be achieved locklessly.
|
||||
struct MemoryWrite {
|
||||
uint32_t time;
|
||||
uint16_t address;
|
||||
uint8_t value;
|
||||
bool enabled;
|
||||
};
|
||||
static_assert(sizeof(MemoryWrite) == 8);
|
||||
constexpr static int StoreBufferSize = 16384;
|
||||
// Use a circular buffer for piping memory alterations onto the audio
|
||||
// thread; it would be prohibitive to defer every write individually.
|
||||
//
|
||||
// Assumed: on most modern architectures, an atomic 64-bit read or
|
||||
// write can be achieved locklessly.
|
||||
struct MemoryWrite {
|
||||
uint32_t time;
|
||||
uint16_t address;
|
||||
uint8_t value;
|
||||
bool enabled;
|
||||
};
|
||||
static_assert(sizeof(MemoryWrite) == 8);
|
||||
constexpr static int StoreBufferSize = 16384;
|
||||
|
||||
std::atomic<MemoryWrite> pending_stores_[StoreBufferSize];
|
||||
uint32_t pending_store_read_ = 0, pending_store_read_time_ = 0;
|
||||
uint32_t pending_store_write_ = 0, pending_store_write_time_ = 0;
|
||||
std::atomic<MemoryWrite> pending_stores_[StoreBufferSize];
|
||||
uint32_t pending_store_read_ = 0, pending_store_read_time_ = 0;
|
||||
uint32_t pending_store_write_ = 0, pending_store_write_time_ = 0;
|
||||
|
||||
// Maintain state both 'locally' (i.e. on the emulation thread) and
|
||||
// 'remotely' (i.e. on the audio thread).
|
||||
struct EnsoniqState {
|
||||
uint8_t ram_[65536];
|
||||
struct Oscillator {
|
||||
uint32_t position;
|
||||
// Maintain state both 'locally' (i.e. on the emulation thread) and
|
||||
// 'remotely' (i.e. on the audio thread).
|
||||
struct EnsoniqState {
|
||||
uint8_t ram_[65536];
|
||||
struct Oscillator {
|
||||
uint32_t position;
|
||||
|
||||
// Programmer-set values.
|
||||
uint16_t velocity;
|
||||
uint8_t volume;
|
||||
uint8_t address;
|
||||
uint8_t control;
|
||||
uint8_t table_size;
|
||||
// Programmer-set values.
|
||||
uint16_t velocity;
|
||||
uint8_t volume;
|
||||
uint8_t address;
|
||||
uint8_t control;
|
||||
uint8_t table_size;
|
||||
|
||||
// Derived state.
|
||||
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.
|
||||
bool interrupt_request = false; // Will be non-zero if this channel would request an interrupt, were
|
||||
// it currently enabled to do so.
|
||||
// Derived state.
|
||||
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.
|
||||
bool interrupt_request = false; // Will be non-zero if this channel would request an interrupt, were
|
||||
// it currently enabled to do so.
|
||||
|
||||
uint8_t sample(uint8_t *ram);
|
||||
int16_t output(uint8_t *ram);
|
||||
} oscillators[32];
|
||||
uint8_t sample(uint8_t *ram);
|
||||
int16_t output(uint8_t *ram);
|
||||
} oscillators[32];
|
||||
|
||||
// Some of these aren't actually needed on both threads.
|
||||
uint8_t control = 0;
|
||||
int oscillator_count = 1;
|
||||
// Some of these aren't actually needed on both threads.
|
||||
uint8_t control = 0;
|
||||
int oscillator_count = 1;
|
||||
|
||||
void set_register(uint16_t address, uint8_t value);
|
||||
} local_, remote_;
|
||||
void set_register(uint16_t address, uint8_t value);
|
||||
} local_, remote_;
|
||||
|
||||
// Functions to update an EnsoniqState; these don't belong to the state itself
|
||||
// because they also access the pending stores (inter alia).
|
||||
template <Outputs::Speaker::Action action>
|
||||
void generate_audio(size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
void skip_audio(EnsoniqState &state, size_t number_of_samples);
|
||||
// Functions to update an EnsoniqState; these don't belong to the state itself
|
||||
// because they also access the pending stores (inter alia).
|
||||
template <Outputs::Speaker::Action action>
|
||||
void generate_audio(size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
void skip_audio(EnsoniqState &state, size_t number_of_samples);
|
||||
|
||||
// Audio-thread state.
|
||||
int16_t output_range_ = 0;
|
||||
// Audio-thread state.
|
||||
int16_t output_range_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -20,236 +20,236 @@ namespace Apple::IIgs::Video {
|
||||
stretched cycle.
|
||||
*/
|
||||
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:
|
||||
Video();
|
||||
void set_internal_ram(const uint8_t *);
|
||||
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();
|
||||
}
|
||||
|
||||
bool get_is_vertical_blank(Cycles offset);
|
||||
uint8_t get_horizontal_counter(Cycles offset);
|
||||
uint8_t get_vertical_counter(Cycles offset);
|
||||
void clear(uint8_t value) {
|
||||
// Zeroes in bits 5 or 6 clear the respective interrupts.
|
||||
value_ &= value | ~0x60;
|
||||
test();
|
||||
}
|
||||
|
||||
void set_new_video(uint8_t);
|
||||
uint8_t get_new_video();
|
||||
void set_control(uint8_t value) {
|
||||
// Ones in bits 1 or 2 enable the respective interrupts.
|
||||
value_ = (value_ & ~0x6) | (value & 0x6);
|
||||
test();
|
||||
}
|
||||
|
||||
void clear_interrupts(uint8_t);
|
||||
uint8_t get_interrupt_register();
|
||||
void set_interrupt_register(uint8_t);
|
||||
bool get_interrupt_line();
|
||||
uint8_t status() const {
|
||||
return value_;
|
||||
}
|
||||
|
||||
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();
|
||||
bool active() const {
|
||||
return value_ & 0x80;
|
||||
}
|
||||
|
||||
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;
|
||||
void test() {
|
||||
value_ &= 0x7f;
|
||||
if((value_ >> 4) & value_ & 0x6) {
|
||||
value_ |= 0x80;
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
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.
|
||||
// Overall meaning of value is as per the VGC interrupt register, i.e.
|
||||
//
|
||||
// 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:
|
||||
// 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_;
|
||||
|
||||
// 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;
|
||||
int cycles_into_frame_ = 0;
|
||||
const uint8_t *ram_ = nullptr;
|
||||
|
||||
/// 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);
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
// Current pixel output buffer and conceptual format.
|
||||
PixelBufferFormat pixels_format_;
|
||||
uint16_t *pixels_ = nullptr, *next_pixel_ = nullptr;
|
||||
int pixels_start_column_;
|
||||
|
||||
// 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.
|
||||
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.
|
||||
// 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.
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -24,61 +24,61 @@ namespace Apple::Macintosh {
|
||||
a shade less than 4Mhz.
|
||||
*/
|
||||
class Audio: public ::Outputs::Speaker::BufferSource<Audio, false> {
|
||||
public:
|
||||
Audio(Concurrency::AsyncTaskQueue<false> &task_queue);
|
||||
public:
|
||||
Audio(Concurrency::AsyncTaskQueue<false> &task_queue);
|
||||
|
||||
/*!
|
||||
Macintosh audio is (partly) sourced by the same scanning
|
||||
hardware as the video; each line it collects an additional
|
||||
word of memory, half of which is used for audio output.
|
||||
/*!
|
||||
Macintosh audio is (partly) sourced by the same scanning
|
||||
hardware as the video; each line it collects an additional
|
||||
word of memory, half of which is used for audio output.
|
||||
|
||||
Use this method to add a newly-collected sample to the queue.
|
||||
*/
|
||||
void post_sample(uint8_t sample);
|
||||
Use this method to add a newly-collected sample to the queue.
|
||||
*/
|
||||
void post_sample(uint8_t sample);
|
||||
|
||||
/*!
|
||||
Macintosh audio also separately receives an output volume
|
||||
level, in the range 0 to 7.
|
||||
/*!
|
||||
Macintosh audio also separately receives an output volume
|
||||
level, in the range 0 to 7.
|
||||
|
||||
Use this method to set the current output volume.
|
||||
*/
|
||||
void set_volume(int volume);
|
||||
Use this method to set the current output volume.
|
||||
*/
|
||||
void set_volume(int volume);
|
||||
|
||||
/*!
|
||||
A further factor in audio output is the on-off toggle.
|
||||
*/
|
||||
void set_enabled(bool on);
|
||||
/*!
|
||||
A further factor in audio output is the on-off toggle.
|
||||
*/
|
||||
void set_enabled(bool on);
|
||||
|
||||
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
bool is_zero_level() const;
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
bool is_zero_level() const;
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
|
||||
// A queue of fetched samples; read from by one thread,
|
||||
// written to by another.
|
||||
struct {
|
||||
std::array<std::atomic<uint8_t>, 740> buffer;
|
||||
size_t read_pointer = 0, write_pointer = 0;
|
||||
} sample_queue_;
|
||||
// A queue of fetched samples; read from by one thread,
|
||||
// written to by another.
|
||||
struct {
|
||||
std::array<std::atomic<uint8_t>, 740> buffer;
|
||||
size_t read_pointer = 0, write_pointer = 0;
|
||||
} sample_queue_;
|
||||
|
||||
// Emulator-thread stateful variables, to avoid work posting
|
||||
// deferral updates if possible.
|
||||
int posted_volume_ = 0;
|
||||
int posted_enable_mask_ = 0;
|
||||
// Emulator-thread stateful variables, to avoid work posting
|
||||
// deferral updates if possible.
|
||||
int posted_volume_ = 0;
|
||||
int posted_enable_mask_ = 0;
|
||||
|
||||
// Stateful variables, modified from the audio generation
|
||||
// thread only.
|
||||
int volume_ = 0;
|
||||
int enabled_mask_ = 0;
|
||||
std::int16_t output_volume_ = 0;
|
||||
// Stateful variables, modified from the audio generation
|
||||
// thread only.
|
||||
int volume_ = 0;
|
||||
int enabled_mask_ = 0;
|
||||
std::int16_t output_volume_ = 0;
|
||||
|
||||
std::int16_t volume_multiplier_ = 0;
|
||||
std::size_t subcycle_offset_ = 0;
|
||||
void set_volume_multiplier();
|
||||
std::int16_t volume_multiplier_ = 0;
|
||||
std::size_t subcycle_offset_ = 0;
|
||||
void set_volume_multiplier();
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -15,27 +15,27 @@
|
||||
namespace Apple::Macintosh {
|
||||
|
||||
class DriveSpeedAccumulator {
|
||||
public:
|
||||
/*!
|
||||
Accepts fetched motor control values.
|
||||
*/
|
||||
void post_sample(uint8_t sample);
|
||||
public:
|
||||
/*!
|
||||
Accepts fetched motor control values.
|
||||
*/
|
||||
void post_sample(uint8_t sample);
|
||||
|
||||
struct Delegate {
|
||||
virtual void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) = 0;
|
||||
};
|
||||
/*!
|
||||
Sets the delegate to receive drive speed changes.
|
||||
*/
|
||||
void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
struct Delegate {
|
||||
virtual void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) = 0;
|
||||
};
|
||||
/*!
|
||||
Sets the delegate to receive drive speed changes.
|
||||
*/
|
||||
void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int samples_per_bucket = 20;
|
||||
int sample_count_ = 0;
|
||||
int sample_total_ = 0;
|
||||
Delegate *delegate_ = nullptr;
|
||||
private:
|
||||
static constexpr int samples_per_bucket = 20;
|
||||
int sample_count_ = 0;
|
||||
int sample_total_ = 0;
|
||||
Delegate *delegate_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -83,206 +83,206 @@ enum class Key: uint16_t {
|
||||
};
|
||||
|
||||
class Keyboard {
|
||||
public:
|
||||
void set_input(bool data) {
|
||||
switch(mode_) {
|
||||
case Mode::Waiting:
|
||||
/*
|
||||
"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
|
||||
computer signals that it is ready to begin communication by pulling the Keyboard Data line low."
|
||||
*/
|
||||
if(!data) {
|
||||
mode_ = Mode::AcceptingCommand;
|
||||
phase_ = 0;
|
||||
command_ = 0;
|
||||
}
|
||||
break;
|
||||
public:
|
||||
void set_input(bool data) {
|
||||
switch(mode_) {
|
||||
case Mode::Waiting:
|
||||
/*
|
||||
"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
|
||||
computer signals that it is ready to begin communication by pulling the Keyboard Data line low."
|
||||
*/
|
||||
if(!data) {
|
||||
mode_ = Mode::AcceptingCommand;
|
||||
phase_ = 0;
|
||||
command_ = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case Mode::AcceptingCommand:
|
||||
/* Note value, so that it can be latched upon a clock transition. */
|
||||
data_input_ = data;
|
||||
break;
|
||||
case Mode::AcceptingCommand:
|
||||
/* Note value, so that it can be latched upon a clock transition. */
|
||||
data_input_ = data;
|
||||
break;
|
||||
|
||||
case Mode::AwaitingEndOfCommand:
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
if(data) {
|
||||
mode_ = Mode::PerformingCommand;
|
||||
phase_ = 0;
|
||||
}
|
||||
break;
|
||||
case Mode::AwaitingEndOfCommand:
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
if(data) {
|
||||
mode_ = Mode::PerformingCommand;
|
||||
phase_ = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
case Mode::SendingResponse:
|
||||
/* This line isn't currently an input; do nothing. */
|
||||
break;
|
||||
}
|
||||
default:
|
||||
case Mode::SendingResponse:
|
||||
/* This line isn't currently an input; do nothing. */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool get_clock() {
|
||||
return clock_output_;
|
||||
bool get_clock() {
|
||||
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() {
|
||||
return !!(response_ & 0x80);
|
||||
void enqueue_key_state(uint16_t key, bool is_pressed) {
|
||||
// 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));
|
||||
}
|
||||
|
||||
/*!
|
||||
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;
|
||||
private:
|
||||
/// 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
|
||||
/// and instant as the same command.
|
||||
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 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;
|
||||
case 0x16: // Model number.
|
||||
return
|
||||
0x01 | // b0: always 1
|
||||
(1 << 1) | // keyboard model number
|
||||
(1 << 4); // next device number
|
||||
// (b7 not set => no next device)
|
||||
|
||||
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;
|
||||
}
|
||||
case 0x36: // Test
|
||||
return 0x7d; // 0x7d = ACK, 0x77 = not ACK.
|
||||
}
|
||||
return 0x7b; // No key transition.
|
||||
}
|
||||
|
||||
void enqueue_key_state(uint16_t key, bool is_pressed) {
|
||||
// Front insert; messages will be pop_back'd.
|
||||
std::lock_guard lock(key_queue_mutex_);
|
||||
/// 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;
|
||||
|
||||
// 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));
|
||||
}
|
||||
/// 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;
|
||||
|
||||
private:
|
||||
/// 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
|
||||
/// and instant as the same command.
|
||||
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;
|
||||
/// 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;
|
||||
|
||||
case 0x16: // Model number.
|
||||
return
|
||||
0x01 | // b0: always 1
|
||||
(1 << 1) | // keyboard model number
|
||||
(1 << 4); // next device number
|
||||
// (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_;
|
||||
/// 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
@ -28,76 +28,76 @@ constexpr int sync_end = 38;
|
||||
This class also collects audio and 400kb drive-speed data, forwarding those values.
|
||||
*/
|
||||
class Video {
|
||||
public:
|
||||
/*!
|
||||
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.
|
||||
*/
|
||||
Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator);
|
||||
public:
|
||||
/*!
|
||||
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.
|
||||
*/
|
||||
Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator);
|
||||
|
||||
/*!
|
||||
Sets the target device for video data.
|
||||
*/
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
/*!
|
||||
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;
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/*!
|
||||
Produces the next @c duration period of pixels.
|
||||
*/
|
||||
void run_for(HalfCycles duration);
|
||||
/*!
|
||||
Produces the next @c duration period of pixels.
|
||||
*/
|
||||
void run_for(HalfCycles duration);
|
||||
|
||||
/*!
|
||||
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);
|
||||
/*!
|
||||
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);
|
||||
|
||||
/*!
|
||||
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.
|
||||
*/
|
||||
void set_ram(uint16_t *ram, uint32_t mask);
|
||||
/*!
|
||||
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.
|
||||
*/
|
||||
void set_ram(uint16_t *ram, uint32_t mask);
|
||||
|
||||
/*!
|
||||
@returns @c true if the video is currently outputting a vertical sync, @c false otherwise.
|
||||
*/
|
||||
bool vsync();
|
||||
/*!
|
||||
@returns @c true if the video is currently outputting a vertical sync, @c false otherwise.
|
||||
*/
|
||||
bool vsync();
|
||||
|
||||
/*
|
||||
@returns @c true if in @c offset half cycles from now, the video will be outputting pixels;
|
||||
@c false otherwise.
|
||||
*/
|
||||
bool is_outputting(HalfCycles offset = HalfCycles(0)) {
|
||||
const auto offset_position = frame_position_ + offset % frame_length;
|
||||
const int column = int((offset_position % line_length).as_integral()) >> 4;
|
||||
const int line = int((offset_position / line_length).as_integral());
|
||||
return line < 342 && column < 32;
|
||||
}
|
||||
/*
|
||||
@returns @c true if in @c offset half cycles from now, the video will be outputting pixels;
|
||||
@c false otherwise.
|
||||
*/
|
||||
bool is_outputting(HalfCycles offset = HalfCycles(0)) {
|
||||
const auto offset_position = frame_position_ + offset % frame_length;
|
||||
const int column = int((offset_position % line_length).as_integral()) >> 4;
|
||||
const int line = int((offset_position / line_length).as_integral());
|
||||
return line < 342 && column < 32;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns the amount of time until there is next a transition on the
|
||||
vsync signal.
|
||||
*/
|
||||
HalfCycles next_sequence_point();
|
||||
/*!
|
||||
@returns the amount of time until there is next a transition on the
|
||||
vsync signal.
|
||||
*/
|
||||
HalfCycles next_sequence_point();
|
||||
|
||||
private:
|
||||
DeferredAudio &audio_;
|
||||
DriveSpeedAccumulator &drive_speed_accumulator_;
|
||||
private:
|
||||
DeferredAudio &audio_;
|
||||
DriveSpeedAccumulator &drive_speed_accumulator_;
|
||||
|
||||
Outputs::CRT::CRT crt_;
|
||||
uint16_t *ram_ = nullptr;
|
||||
uint32_t ram_mask_ = 0;
|
||||
Outputs::CRT::CRT crt_;
|
||||
uint16_t *ram_ = nullptr;
|
||||
uint32_t ram_mask_ = 0;
|
||||
|
||||
HalfCycles frame_position_;
|
||||
HalfCycles frame_position_;
|
||||
|
||||
size_t video_address_ = 0;
|
||||
size_t audio_address_ = 0;
|
||||
size_t video_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_audio_buffer_ = false;
|
||||
bool use_alternate_screen_buffer_ = false;
|
||||
bool use_alternate_audio_buffer_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -36,39 +36,39 @@ namespace {
|
||||
namespace Atari2600 {
|
||||
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick(Bus *bus, std::size_t shift, std::size_t fire_tia_input) :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Up),
|
||||
Input(Input::Down),
|
||||
Input(Input::Left),
|
||||
Input(Input::Right),
|
||||
Input(Input::Fire)
|
||||
}),
|
||||
bus_(bus), shift_(shift), fire_tia_input_(fire_tia_input) {}
|
||||
public:
|
||||
Joystick(Bus *bus, std::size_t shift, std::size_t fire_tia_input) :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Up),
|
||||
Input(Input::Down),
|
||||
Input(Input::Left),
|
||||
Input(Input::Right),
|
||||
Input(Input::Fire)
|
||||
}),
|
||||
bus_(bus), shift_(shift), fire_tia_input_(fire_tia_input) {}
|
||||
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
switch(digital_input.type) {
|
||||
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::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;
|
||||
void did_set_input(const Input &digital_input, bool is_active) final {
|
||||
switch(digital_input.type) {
|
||||
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::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;
|
||||
|
||||
// TODO: latching
|
||||
case Input::Fire:
|
||||
if(is_active)
|
||||
bus_->tia_input_value_[fire_tia_input_] &= ~0x80;
|
||||
else
|
||||
bus_->tia_input_value_[fire_tia_input_] |= 0x80;
|
||||
break;
|
||||
// TODO: latching
|
||||
case Input::Fire:
|
||||
if(is_active)
|
||||
bus_->tia_input_value_[fire_tia_input_] &= ~0x80;
|
||||
else
|
||||
bus_->tia_input_value_[fire_tia_input_] |= 0x80;
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Bus *bus_;
|
||||
std::size_t shift_, fire_tia_input_;
|
||||
private:
|
||||
Bus *bus_;
|
||||
std::size_t shift_, fire_tia_input_;
|
||||
};
|
||||
|
||||
using Target = Analyser::Static::Atari2600::Target;
|
||||
@ -79,133 +79,133 @@ class ConcreteMachine:
|
||||
public MachineTypes::AudioProducer,
|
||||
public MachineTypes::ScanProducer,
|
||||
public MachineTypes::JoystickMachine {
|
||||
public:
|
||||
ConcreteMachine(const Target &target) : frequency_mismatch_warner_(*this) {
|
||||
const std::vector<uint8_t> &rom = target.media.cartridges.front()->get_segments().front().data;
|
||||
public:
|
||||
ConcreteMachine(const Target &target) : frequency_mismatch_warner_(*this) {
|
||||
const std::vector<uint8_t> &rom = target.media.cartridges.front()->get_segments().front().data;
|
||||
|
||||
using PagingModel = Target::PagingModel;
|
||||
switch(target.paging_model) {
|
||||
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::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::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::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::Tigervision: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Tigervision>>(rom); break;
|
||||
using PagingModel = Target::PagingModel;
|
||||
switch(target.paging_model) {
|
||||
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::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::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::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::Tigervision: bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Tigervision>>(rom); break;
|
||||
|
||||
case PagingModel::Atari8k:
|
||||
if(target.uses_superchip) {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari8kSuperChip>>(rom);
|
||||
} else {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari8k>>(rom);
|
||||
}
|
||||
break;
|
||||
case PagingModel::Atari16k:
|
||||
if(target.uses_superchip) {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari16kSuperChip>>(rom);
|
||||
} else {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari16k>>(rom);
|
||||
}
|
||||
break;
|
||||
case PagingModel::Atari32k:
|
||||
if(target.uses_superchip) {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari32kSuperChip>>(rom);
|
||||
} else {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari32k>>(rom);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
joysticks_.emplace_back(new Joystick(bus_.get(), 0, 0));
|
||||
joysticks_.emplace_back(new Joystick(bus_.get(), 4, 1));
|
||||
|
||||
set_is_ntsc(is_ntsc_);
|
||||
case PagingModel::Atari8k:
|
||||
if(target.uses_superchip) {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari8kSuperChip>>(rom);
|
||||
} else {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari8k>>(rom);
|
||||
}
|
||||
break;
|
||||
case PagingModel::Atari16k:
|
||||
if(target.uses_superchip) {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari16kSuperChip>>(rom);
|
||||
} else {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari16k>>(rom);
|
||||
}
|
||||
break;
|
||||
case PagingModel::Atari32k:
|
||||
if(target.uses_superchip) {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari32kSuperChip>>(rom);
|
||||
} else {
|
||||
bus_ = std::make_unique<Cartridge::Cartridge<Cartridge::Atari32k>>(rom);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
return joysticks_;
|
||||
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 {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
bool get_switch_is_enabled(Atari2600Switch input) const final {
|
||||
uint8_t port_input = bus_->mos6532_.get_port_input(1);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
bool get_switch_is_enabled(Atari2600Switch input) const final {
|
||||
uint8_t port_input = bus_->mos6532_.get_port_input(1);
|
||||
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 {
|
||||
bus_->set_reset_line(state);
|
||||
}
|
||||
|
||||
void set_reset_switch(bool state) final {
|
||||
bus_->set_reset_line(state);
|
||||
}
|
||||
// to satisfy CRTMachine::Machine
|
||||
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
|
||||
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);
|
||||
}
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return bus_->tia_.get_scaled_scan_status() / 3.0f;
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return bus_->tia_.get_scaled_scan_status() / 3.0f;
|
||||
}
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &bus_->speaker_;
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
return &bus_->speaker_;
|
||||
}
|
||||
void run_for(const Cycles cycles) final {
|
||||
bus_->run_for(cycles);
|
||||
bus_->apply_confidence(confidence_counter_);
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) final {
|
||||
bus_->run_for(cycles);
|
||||
bus_->apply_confidence(confidence_counter_);
|
||||
}
|
||||
void flush_output(int) final {
|
||||
bus_->flush();
|
||||
}
|
||||
|
||||
void flush_output(int) final {
|
||||
bus_->flush();
|
||||
}
|
||||
void register_crt_frequency_mismatch() {
|
||||
is_ntsc_ ^= true;
|
||||
set_is_ntsc(is_ntsc_);
|
||||
}
|
||||
|
||||
void register_crt_frequency_mismatch() {
|
||||
is_ntsc_ ^= true;
|
||||
set_is_ntsc(is_ntsc_);
|
||||
}
|
||||
float get_confidence() final {
|
||||
return confidence_counter_.get_confidence();
|
||||
}
|
||||
|
||||
float get_confidence() final {
|
||||
return confidence_counter_.get_confidence();
|
||||
}
|
||||
private:
|
||||
// The bus.
|
||||
std::unique_ptr<Bus> bus_;
|
||||
|
||||
private:
|
||||
// The bus.
|
||||
std::unique_ptr<Bus> bus_;
|
||||
// Output frame rate tracker.
|
||||
Outputs::CRT::CRTFrequencyMismatchWarner<ConcreteMachine> frequency_mismatch_warner_;
|
||||
bool is_ntsc_ = true;
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
|
||||
// Output frame rate tracker.
|
||||
Outputs::CRT::CRTFrequencyMismatchWarner<ConcreteMachine> frequency_mismatch_warner_;
|
||||
bool is_ntsc_ = true;
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
// a confidence counter
|
||||
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
|
||||
|
||||
// a confidence counter
|
||||
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
|
||||
|
||||
void set_is_ntsc(bool is_ntsc) {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
void set_is_ntsc(bool is_ntsc) {
|
||||
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;
|
||||
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
@ -19,93 +19,93 @@
|
||||
namespace Atari::ST {
|
||||
|
||||
class DMAController: public WD::WD1770::Delegate, public ClockingHint::Source, public ClockingHint::Observer {
|
||||
public:
|
||||
DMAController();
|
||||
public:
|
||||
DMAController();
|
||||
|
||||
uint16_t read(int address);
|
||||
void write(int address, uint16_t value);
|
||||
void run_for(HalfCycles duration);
|
||||
uint16_t read(int address);
|
||||
void write(int address, uint16_t value);
|
||||
void run_for(HalfCycles duration);
|
||||
|
||||
bool get_interrupt_line();
|
||||
bool get_bus_request_line();
|
||||
bool get_interrupt_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
|
||||
is of size @c size.
|
||||
/*!
|
||||
Indicates that the DMA controller has been granted bus access to the block of memory at @c ram, which
|
||||
is of size @c size.
|
||||
|
||||
@returns The number of words read or written.
|
||||
*/
|
||||
int bus_grant(uint16_t *ram, size_t size);
|
||||
@returns The number of words read or written.
|
||||
*/
|
||||
int bus_grant(uint16_t *ram, size_t size);
|
||||
|
||||
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_drive_selection(bool drive1, bool drive2, bool side2);
|
||||
void set_floppy_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
|
||||
|
||||
struct Delegate {
|
||||
virtual void dma_controller_did_change_output(DMAController *) = 0;
|
||||
};
|
||||
void set_delegate(Delegate *delegate);
|
||||
struct Delegate {
|
||||
virtual void dma_controller_did_change_output(DMAController *) = 0;
|
||||
};
|
||||
void set_delegate(Delegate *delegate);
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
|
||||
// ClockingHint::Source.
|
||||
ClockingHint::Preference preferred_clocking() const final;
|
||||
// ClockingHint::Source.
|
||||
ClockingHint::Preference preferred_clocking() const final;
|
||||
|
||||
private:
|
||||
HalfCycles running_time_;
|
||||
struct WD1772: public WD::WD1770 {
|
||||
WD1772(): WD::WD1770(WD::WD1770::P1772) {
|
||||
emplace_drives(2, 8000000, 300, 2);
|
||||
set_is_double_density(true); // TODO: is this selectable on the ST?
|
||||
}
|
||||
private:
|
||||
HalfCycles running_time_;
|
||||
struct WD1772: public WD::WD1770 {
|
||||
WD1772(): WD::WD1770(WD::WD1770::P1772) {
|
||||
emplace_drives(2, 8000000, 300, 2);
|
||||
set_is_double_density(true); // TODO: is this selectable on the ST?
|
||||
}
|
||||
|
||||
void set_motor_on(bool motor_on) final {
|
||||
for_all_drives([motor_on] (Storage::Disk::Drive &drive, size_t) {
|
||||
drive.set_motor_on(motor_on);
|
||||
});
|
||||
}
|
||||
void set_motor_on(bool motor_on) final {
|
||||
for_all_drives([motor_on] (Storage::Disk::Drive &drive, size_t) {
|
||||
drive.set_motor_on(motor_on);
|
||||
});
|
||||
}
|
||||
|
||||
void set_floppy_drive_selection(bool drive1, bool drive2, bool side2) {
|
||||
set_drive(
|
||||
(drive1 ? 1 : 0) |
|
||||
(drive2 ? 2 : 0)
|
||||
);
|
||||
void set_floppy_drive_selection(bool drive1, bool drive2, bool side2) {
|
||||
set_drive(
|
||||
(drive1 ? 1 : 0) |
|
||||
(drive2 ? 2 : 0)
|
||||
);
|
||||
|
||||
for_all_drives([side2] (Storage::Disk::Drive &drive, size_t) {
|
||||
drive.set_head(side2);
|
||||
});
|
||||
}
|
||||
for_all_drives([side2] (Storage::Disk::Drive &drive, size_t) {
|
||||
drive.set_head(side2);
|
||||
});
|
||||
}
|
||||
|
||||
void set_activity_observer(Activity::Observer *observer) {
|
||||
get_drive(0).set_activity_observer(observer, "Internal", true);
|
||||
get_drive(1).set_activity_observer(observer, "External", true);
|
||||
}
|
||||
void set_activity_observer(Activity::Observer *observer) {
|
||||
get_drive(0).set_activity_observer(observer, "Internal", true);
|
||||
get_drive(1).set_activity_observer(observer, "External", true);
|
||||
}
|
||||
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
|
||||
get_drive(drive).set_disk(disk);
|
||||
}
|
||||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
|
||||
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;
|
||||
bool interrupt_line_ = false;
|
||||
bool bus_request_line_ = false;
|
||||
Delegate *delegate_ = nullptr;
|
||||
bool interrupt_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.
|
||||
struct Buffer {
|
||||
uint8_t contents[16];
|
||||
bool is_full = false;
|
||||
} buffer_[2];
|
||||
int active_buffer_ = 0;
|
||||
int bytes_received_ = 0;
|
||||
bool error_ = false;
|
||||
int address_ = 0;
|
||||
int byte_count_ = 0;
|
||||
// MARK: - DMA State.
|
||||
struct Buffer {
|
||||
uint8_t contents[16];
|
||||
bool is_full = false;
|
||||
} buffer_[2];
|
||||
int active_buffer_ = 0;
|
||||
int bytes_received_ = 0;
|
||||
bool error_ = false;
|
||||
int address_ = 0;
|
||||
int byte_count_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -54,144 +54,144 @@ class IntelligentKeyboard:
|
||||
public Serial::Line<false>::ReadDelegate,
|
||||
public ClockingHint::Source,
|
||||
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:
|
||||
IntelligentKeyboard(Serial::Line<false> &input, Serial::Line<false> &output);
|
||||
ClockingHint::Preference preferred_clocking() const final;
|
||||
void run_for(HalfCycles duration);
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
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);
|
||||
class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
|
||||
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const final;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
|
||||
return joysticks_;
|
||||
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:
|
||||
// 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:
|
||||
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_;
|
||||
uint8_t state_ = 0x00;
|
||||
uint8_t returned_state_ = 0x00;
|
||||
};
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -31,220 +31,220 @@ struct LineLength {
|
||||
(hopefully) to a subsystem.
|
||||
*/
|
||||
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:
|
||||
Video();
|
||||
VideoStream(Outputs::CRT::CRT &crt, uint16_t *palette) : crt_(crt), palette_(palette) {}
|
||||
|
||||
/*!
|
||||
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
|
||||
enum class OutputMode {
|
||||
Sync, Blank, ColourBurst, Pixels,
|
||||
};
|
||||
|
||||
struct RangeObserver {
|
||||
/// Indicates to the observer that the memory access range has changed.
|
||||
virtual void video_did_change_access_range(Video *) = 0;
|
||||
};
|
||||
/// Sets the current data format for the shifter. Changes in output BPP flush the shifter.
|
||||
void set_bpp(OutputBpp bpp);
|
||||
|
||||
/// Sets a range observer, which is an actor that will be notified if the memory access range changes.
|
||||
void set_range_observer(RangeObserver *);
|
||||
/// Outputs signal of type @c mode for @c duration.
|
||||
void output(int duration, OutputMode mode);
|
||||
|
||||
struct Range {
|
||||
uint32_t low_address, high_address;
|
||||
};
|
||||
/*!
|
||||
@returns the range of addresses that the video might read from.
|
||||
*/
|
||||
Range get_memory_access_range();
|
||||
/// 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:
|
||||
DeferredQueue<HalfCycles> deferrer_;
|
||||
// The target CRT and the palette to use.
|
||||
Outputs::CRT::CRT &crt_;
|
||||
uint16_t *palette_ = nullptr;
|
||||
|
||||
Outputs::CRT::CRT crt_;
|
||||
RangeObserver *range_observer_ = nullptr;
|
||||
// Internal stateful processes.
|
||||
void generate(int duration, OutputMode mode, bool is_terminal);
|
||||
|
||||
uint16_t raw_palette_[16];
|
||||
uint16_t palette_[16];
|
||||
int base_address_ = 0;
|
||||
int previous_base_address_ = 0;
|
||||
int current_address_ = 0;
|
||||
void flush_border();
|
||||
void flush_pixels();
|
||||
void shift(int duration);
|
||||
void output_pixels(int duration);
|
||||
|
||||
uint16_t *ram_ = nullptr;
|
||||
int ram_mask_ = 0;
|
||||
// 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];
|
||||
};
|
||||
|
||||
int x_ = 0, y_ = 0, next_y_ = 0;
|
||||
bool load_ = false;
|
||||
int load_base_ = 0;
|
||||
// Internal state for handling output serialisation.
|
||||
uint16_t *pixel_buffer_ = nullptr;
|
||||
int pixel_pointer_ = 0;
|
||||
} video_stream_;
|
||||
|
||||
uint16_t video_mode_ = 0;
|
||||
uint16_t sync_mode_ = 0;
|
||||
/// 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_;
|
||||
|
||||
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:
|
||||
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;
|
||||
friend class ::VideoTester;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
class SerialPortVIA: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
public:
|
||||
SerialPortVIA(MOS::MOS6522::MOS6522<SerialPortVIA> &via);
|
||||
public:
|
||||
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_serial_line_state(::Commodore::Serial::Line, bool);
|
||||
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_port(const std::shared_ptr<::Commodore::Serial::Port> &);
|
||||
void set_serial_port(const std::shared_ptr<::Commodore::Serial::Port> &);
|
||||
|
||||
private:
|
||||
MOS::MOS6522::MOS6522<SerialPortVIA> &via_;
|
||||
uint8_t port_b_ = 0x0;
|
||||
std::weak_ptr<::Commodore::Serial::Port> serial_port_;
|
||||
bool attention_acknowledge_level_ = false;
|
||||
bool attention_level_input_ = true;
|
||||
bool data_level_output_ = false;
|
||||
private:
|
||||
MOS::MOS6522::MOS6522<SerialPortVIA> &via_;
|
||||
uint8_t port_b_ = 0x0;
|
||||
std::weak_ptr<::Commodore::Serial::Port> serial_port_;
|
||||
bool attention_acknowledge_level_ = false;
|
||||
bool attention_level_input_ = true;
|
||||
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.
|
||||
*/
|
||||
class DriveVIA: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
public:
|
||||
class Delegate {
|
||||
public:
|
||||
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;
|
||||
};
|
||||
void set_delegate(Delegate *);
|
||||
public:
|
||||
class Delegate {
|
||||
public:
|
||||
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;
|
||||
};
|
||||
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_data_input(uint8_t);
|
||||
bool get_should_set_overflow();
|
||||
bool get_motor_enabled();
|
||||
void set_sync_detected(bool);
|
||||
void set_data_input(uint8_t);
|
||||
bool get_should_set_overflow();
|
||||
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:
|
||||
uint8_t port_b_ = 0xff, port_a_ = 0xff;
|
||||
bool should_set_overflow_ = false;
|
||||
bool drive_motor_ = false;
|
||||
uint8_t previous_port_b_output_ = 0;
|
||||
Delegate *delegate_ = nullptr;
|
||||
Activity::Observer *observer_ = nullptr;
|
||||
private:
|
||||
uint8_t port_b_ = 0xff, port_a_ = 0xff;
|
||||
bool should_set_overflow_ = false;
|
||||
bool drive_motor_ = false;
|
||||
uint8_t previous_port_b_output_ = 0;
|
||||
Delegate *delegate_ = nullptr;
|
||||
Activity::Observer *observer_ = nullptr;
|
||||
};
|
||||
|
||||
/*!
|
||||
An implementation of the C1540's serial port; this connects incoming line levels to the serial-port VIA.
|
||||
*/
|
||||
class SerialPort : public ::Commodore::Serial::Port {
|
||||
public:
|
||||
void set_input(::Commodore::Serial::Line, ::Commodore::Serial::LineLevel);
|
||||
void set_serial_port_via(const std::shared_ptr<SerialPortVIA> &);
|
||||
public:
|
||||
void set_input(::Commodore::Serial::Line, ::Commodore::Serial::LineLevel);
|
||||
void set_serial_port_via(const std::shared_ptr<SerialPortVIA> &);
|
||||
|
||||
private:
|
||||
std::weak_ptr<SerialPortVIA> serial_port_VIA_;
|
||||
private:
|
||||
std::weak_ptr<SerialPortVIA> serial_port_VIA_;
|
||||
};
|
||||
|
||||
class MachineBase:
|
||||
@ -124,38 +124,38 @@ class MachineBase:
|
||||
public DriveVIA::Delegate,
|
||||
public Storage::Disk::Controller {
|
||||
|
||||
public:
|
||||
MachineBase(Personality personality, const ROM::Map &roms);
|
||||
public:
|
||||
MachineBase(Personality personality, const ROM::Map &roms);
|
||||
|
||||
// to satisfy CPU::MOS6502::Processor
|
||||
Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value);
|
||||
// to satisfy CPU::MOS6502::Processor
|
||||
Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value);
|
||||
|
||||
// to satisfy MOS::MOS6522::Delegate
|
||||
virtual void mos6522_did_change_interrupt_status(void *mos6522);
|
||||
// to satisfy MOS::MOS6522::Delegate
|
||||
virtual void mos6522_did_change_interrupt_status(void *mos6522);
|
||||
|
||||
// to satisfy DriveVIA::Delegate
|
||||
void drive_via_did_step_head(void *driveVIA, int direction);
|
||||
void drive_via_did_set_data_density(void *driveVIA, int density);
|
||||
// to satisfy DriveVIA::Delegate
|
||||
void drive_via_did_step_head(void *driveVIA, int direction);
|
||||
void drive_via_did_set_data_density(void *driveVIA, int density);
|
||||
|
||||
/// Attaches the activity observer to this C1540.
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
/// Attaches the activity observer to this C1540.
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
|
||||
protected:
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, MachineBase, false> m6502_;
|
||||
protected:
|
||||
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, MachineBase, false> m6502_;
|
||||
|
||||
uint8_t ram_[0x800];
|
||||
uint8_t rom_[0x4000];
|
||||
uint8_t ram_[0x800];
|
||||
uint8_t rom_[0x4000];
|
||||
|
||||
std::shared_ptr<SerialPortVIA> serial_port_VIA_port_handler_;
|
||||
std::shared_ptr<SerialPort> serial_port_;
|
||||
DriveVIA drive_VIA_port_handler_;
|
||||
std::shared_ptr<SerialPortVIA> serial_port_VIA_port_handler_;
|
||||
std::shared_ptr<SerialPort> serial_port_;
|
||||
DriveVIA drive_VIA_port_handler_;
|
||||
|
||||
MOS::MOS6522::MOS6522<DriveVIA> drive_VIA_;
|
||||
MOS::MOS6522::MOS6522<SerialPortVIA> serial_port_VIA_;
|
||||
MOS::MOS6522::MOS6522<DriveVIA> drive_VIA_;
|
||||
MOS::MOS6522::MOS6522<SerialPortVIA> serial_port_VIA_;
|
||||
|
||||
int shift_register_ = 0, bit_window_offset_;
|
||||
virtual void process_input_bit(int value);
|
||||
virtual void process_index_hole();
|
||||
int shift_register_ = 0, bit_window_offset_;
|
||||
virtual void process_input_bit(int value);
|
||||
virtual void process_index_hole();
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -209,17 +209,17 @@ struct Description {
|
||||
/// plus all the fields provided as @c flags .
|
||||
std::string description(int flags) const;
|
||||
|
||||
private:
|
||||
template <typename FileNameT, typename CRC32T> Description(
|
||||
Name name, std::string machine_name, std::string descriptive_name, FileNameT file_names, size_t size, CRC32T crc32s = uint32_t(0)
|
||||
) : name{name}, machine_name{machine_name}, descriptive_name{descriptive_name}, file_names{file_names}, size{size}, crc32s{crc32s} {
|
||||
// Slightly lazy: deal with the case where the constructor wasn't provided with any
|
||||
// CRCs by spotting that the set has exactly one member, which has value 0. The alternative
|
||||
// would be to provide a partial specialisation that never put anything into the set.
|
||||
if(this->crc32s.size() == 1 && !*this->crc32s.begin()) {
|
||||
this->crc32s.clear();
|
||||
}
|
||||
private:
|
||||
template <typename FileNameT, typename CRC32T> Description(
|
||||
Name name, std::string machine_name, std::string descriptive_name, FileNameT file_names, size_t size, CRC32T crc32s = uint32_t(0)
|
||||
) : name{name}, machine_name{machine_name}, descriptive_name{descriptive_name}, file_names{file_names}, size{size}, crc32s{crc32s} {
|
||||
// Slightly lazy: deal with the case where the constructor wasn't provided with any
|
||||
// CRCs by spotting that the set has exactly one member, which has value 0. The alternative
|
||||
// would be to provide a partial specialisation that never put anything into the set.
|
||||
if(this->crc32s.size() == 1 && !*this->crc32s.begin()) {
|
||||
this->crc32s.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// @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'*').
|
||||
std::wstring description(int description_flags, wchar_t bullet_point);
|
||||
|
||||
private:
|
||||
struct Node {
|
||||
enum class Type {
|
||||
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;
|
||||
}
|
||||
}
|
||||
private:
|
||||
struct Node {
|
||||
enum class Type {
|
||||
Any, All, One
|
||||
};
|
||||
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);
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -14,15 +14,15 @@
|
||||
namespace Utility {
|
||||
|
||||
class StringSerialiser {
|
||||
public:
|
||||
StringSerialiser(const std::string &source, bool use_linefeed_only = false);
|
||||
public:
|
||||
StringSerialiser(const std::string &source, bool use_linefeed_only = false);
|
||||
|
||||
uint8_t head();
|
||||
bool advance();
|
||||
uint8_t head();
|
||||
bool advance();
|
||||
|
||||
private:
|
||||
std::string input_string_;
|
||||
std::size_t input_string_pointer_ = 0;
|
||||
private:
|
||||
std::string input_string_;
|
||||
std::size_t input_string_pointer_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -15,55 +15,55 @@
|
||||
namespace Machine {
|
||||
|
||||
template<typename T> class TypedDynamicMachine: public ::Machine::DynamicMachine {
|
||||
public:
|
||||
TypedDynamicMachine(std::unique_ptr<T> &&machine) : machine_(std::move(machine)) {}
|
||||
T *get() { return machine_.get(); }
|
||||
public:
|
||||
TypedDynamicMachine(std::unique_ptr<T> &&machine) : machine_(std::move(machine)) {}
|
||||
T *get() { return machine_.get(); }
|
||||
|
||||
TypedDynamicMachine() : TypedDynamicMachine(nullptr) {}
|
||||
TypedDynamicMachine(TypedDynamicMachine &&rhs) : machine_(std::move(rhs.machine_)) {}
|
||||
TypedDynamicMachine &operator=(TypedDynamicMachine &&rhs) {
|
||||
machine_ = std::move(rhs.machine_);
|
||||
return *this;
|
||||
}
|
||||
TypedDynamicMachine() : TypedDynamicMachine(nullptr) {}
|
||||
TypedDynamicMachine(TypedDynamicMachine &&rhs) : machine_(std::move(rhs.machine_)) {}
|
||||
TypedDynamicMachine &operator=(TypedDynamicMachine &&rhs) {
|
||||
machine_ = std::move(rhs.machine_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
#define Provide(type, name) \
|
||||
type *name() final { \
|
||||
return get<type>(); \
|
||||
}
|
||||
type *name() final { \
|
||||
return get<type>(); \
|
||||
}
|
||||
|
||||
Provide(Activity::Source, activity_source)
|
||||
Provide(Configurable::Device, configurable_device)
|
||||
Provide(MachineTypes::TimedMachine, timed_machine)
|
||||
Provide(MachineTypes::ScanProducer, scan_producer)
|
||||
Provide(MachineTypes::AudioProducer, audio_producer)
|
||||
Provide(MachineTypes::JoystickMachine, joystick_machine)
|
||||
Provide(MachineTypes::KeyboardMachine, keyboard_machine)
|
||||
Provide(MachineTypes::MouseMachine, mouse_machine)
|
||||
Provide(MachineTypes::MediaTarget, media_target)
|
||||
Provide(Activity::Source, activity_source)
|
||||
Provide(Configurable::Device, configurable_device)
|
||||
Provide(MachineTypes::TimedMachine, timed_machine)
|
||||
Provide(MachineTypes::ScanProducer, scan_producer)
|
||||
Provide(MachineTypes::AudioProducer, audio_producer)
|
||||
Provide(MachineTypes::JoystickMachine, joystick_machine)
|
||||
Provide(MachineTypes::KeyboardMachine, keyboard_machine)
|
||||
Provide(MachineTypes::MouseMachine, mouse_machine)
|
||||
Provide(MachineTypes::MediaTarget, media_target)
|
||||
|
||||
#undef Provide
|
||||
|
||||
void *raw_pointer() final {
|
||||
return get();
|
||||
}
|
||||
void *raw_pointer() final {
|
||||
return get();
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Class> Class *get() {
|
||||
return dynamic_cast<Class *>(machine_.get());
|
||||
private:
|
||||
template <typename Class> Class *get() {
|
||||
return dynamic_cast<Class *>(machine_.get());
|
||||
|
||||
// Note to self: the below is not [currently] used
|
||||
// because in practice TypedDynamicMachine is instantiated
|
||||
// with an abstract parent of the actual class.
|
||||
//
|
||||
// TODO: rethink type hiding here. I think I've boxed myself
|
||||
// into an uncomfortable corner.
|
||||
// Note to self: the below is not [currently] used
|
||||
// because in practice TypedDynamicMachine is instantiated
|
||||
// with an abstract parent of the actual class.
|
||||
//
|
||||
// TODO: rethink type hiding here. I think I've boxed myself
|
||||
// into an uncomfortable corner.
|
||||
// if constexpr (std::is_base_of_v<Class, T>) {
|
||||
// return static_cast<Class *>(machine_.get());
|
||||
// } else {
|
||||
// return nullptr;
|
||||
// }
|
||||
}
|
||||
std::unique_ptr<T> machine_;
|
||||
}
|
||||
std::unique_ptr<T> machine_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -63,41 +63,41 @@ class CharacterMapper {
|
||||
fresh key transition is ready to be consumed.
|
||||
*/
|
||||
class Typer {
|
||||
public:
|
||||
class Delegate: public MachineTypes::KeyActions {
|
||||
public:
|
||||
/// Informs the delegate that this typer has reached the end of its content.
|
||||
virtual void typer_reset(Typer *typer) = 0;
|
||||
};
|
||||
public:
|
||||
class Delegate: public MachineTypes::KeyActions {
|
||||
public:
|
||||
/// Informs the delegate that this typer has reached the end of its content.
|
||||
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.
|
||||
void run_for(const HalfCycles duration);
|
||||
/// Advances for @c duration.
|
||||
void run_for(const HalfCycles duration);
|
||||
|
||||
/// Types the next character now, if there is one.
|
||||
/// @returns @c true if there was anything left to type; @c false otherwise.
|
||||
bool type_next_character();
|
||||
/// Types the next character now, if there is one.
|
||||
/// @returns @c true if there was anything left to type; @c false otherwise.
|
||||
bool type_next_character();
|
||||
|
||||
/// Adds the contents of @c str to the end of the current string.
|
||||
void append(const std::string &str);
|
||||
/// Adds the contents of @c str to the end of the current string.
|
||||
void append(const std::string &str);
|
||||
|
||||
const char BeginString = 0x02; // i.e. ASCII start of text
|
||||
const char EndString = 0x03; // i.e. ASCII end of text
|
||||
const char BeginString = 0x02; // i.e. ASCII start of text
|
||||
const char EndString = 0x03; // i.e. ASCII end of text
|
||||
|
||||
private:
|
||||
std::string string_;
|
||||
std::size_t string_pointer_ = 0;
|
||||
private:
|
||||
std::string string_;
|
||||
std::size_t string_pointer_ = 0;
|
||||
|
||||
const HalfCycles frequency_;
|
||||
HalfCycles counter_;
|
||||
int phase_ = 0;
|
||||
const HalfCycles frequency_;
|
||||
HalfCycles counter_;
|
||||
int phase_ = 0;
|
||||
|
||||
Delegate *const delegate_;
|
||||
CharacterMapper &character_mapper_;
|
||||
Delegate *const delegate_;
|
||||
CharacterMapper &character_mapper_;
|
||||
|
||||
uint16_t try_type_next_character();
|
||||
const uint16_t *sequence_for_character(char) const;
|
||||
uint16_t try_type_next_character();
|
||||
const uint16_t *sequence_for_character(char) const;
|
||||
};
|
||||
|
||||
/*!
|
||||
@ -106,47 +106,47 @@ class Typer {
|
||||
*/
|
||||
template <typename CMapper>
|
||||
class TypeRecipient: public Typer::Delegate {
|
||||
protected:
|
||||
template <typename... Args> TypeRecipient(Args&&... args) : character_mapper(std::forward<Args>(args)...) {}
|
||||
protected:
|
||||
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.
|
||||
void add_typer(const std::string &string) {
|
||||
if(!typer_) {
|
||||
typer_ = std::make_unique<Typer>(string, get_typer_delay(string), get_typer_frequency(), character_mapper, this);
|
||||
} else {
|
||||
typer_->append(string);
|
||||
}
|
||||
/// 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) {
|
||||
if(!typer_) {
|
||||
typer_ = std::make_unique<Typer>(string, get_typer_delay(string), get_typer_frequency(), character_mapper, this);
|
||||
} else {
|
||||
typer_->append(string);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns @c true if the character mapper provides a mapping for @c c; @c false otherwise.
|
||||
*/
|
||||
bool can_type(char c) const {
|
||||
const auto sequence = character_mapper.sequence_for_character(c);
|
||||
return sequence && sequence[0] != MachineTypes::MappedKeyboardMachine::KeyNotMapped;
|
||||
}
|
||||
/*!
|
||||
@returns @c true if the character mapper provides a mapping for @c c; @c false otherwise.
|
||||
*/
|
||||
bool can_type(char c) const {
|
||||
const auto sequence = character_mapper.sequence_for_character(c);
|
||||
return sequence && sequence[0] != MachineTypes::MappedKeyboardMachine::KeyNotMapped;
|
||||
}
|
||||
|
||||
/*!
|
||||
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.
|
||||
*/
|
||||
void typer_reset(Typer *) override {
|
||||
clear_all_keys();
|
||||
/*!
|
||||
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.
|
||||
*/
|
||||
void typer_reset(Typer *) override {
|
||||
clear_all_keys();
|
||||
|
||||
// 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.
|
||||
// So shuffle it off into a siding.
|
||||
previous_typer_ = std::move(typer_);
|
||||
typer_ = nullptr;
|
||||
}
|
||||
// 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.
|
||||
// So shuffle it off into a siding.
|
||||
previous_typer_ = std::move(typer_);
|
||||
typer_ = nullptr;
|
||||
}
|
||||
|
||||
virtual HalfCycles get_typer_delay(const std::string &) const { return HalfCycles(0); }
|
||||
virtual HalfCycles get_typer_frequency() const { return HalfCycles(0); }
|
||||
std::unique_ptr<Typer> typer_;
|
||||
virtual HalfCycles get_typer_delay(const std::string &) const { return HalfCycles(0); }
|
||||
virtual HalfCycles get_typer_frequency() const { return HalfCycles(0); }
|
||||
std::unique_ptr<Typer> typer_;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Typer> previous_typer_;
|
||||
CMapper character_mapper;
|
||||
private:
|
||||
std::unique_ptr<Typer> previous_typer_;
|
||||
CMapper character_mapper;
|
||||
};
|
||||
|
||||
}
|
||||
|
127
Numeric/CRC.hpp
127
Numeric/CRC.hpp
@ -26,81 +26,82 @@ constexpr uint8_t reverse_byte(uint8_t byte) {
|
||||
}
|
||||
|
||||
/*! 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 {
|
||||
public:
|
||||
/*!
|
||||
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));
|
||||
for(int c = 0; c < 256; c++) {
|
||||
IntType shift_value = IntType(c << multibyte_shift);
|
||||
for(int b = 0; b < 8; b++) {
|
||||
IntType exclusive_or = (shift_value&top_bit) ? polynomial : 0;
|
||||
shift_value = IntType(shift_value << 1) ^ exclusive_or;
|
||||
}
|
||||
xor_table[c] = shift_value;
|
||||
template <typename IntType, IntType reset_value, IntType output_xor, bool reflect_input, bool reflect_output>
|
||||
class Generator {
|
||||
public:
|
||||
/*!
|
||||
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));
|
||||
for(int c = 0; c < 256; c++) {
|
||||
IntType shift_value = IntType(c << multibyte_shift);
|
||||
for(int b = 0; b < 8; b++) {
|
||||
IntType exclusive_or = (shift_value&top_bit) ? polynomial : 0;
|
||||
shift_value = IntType(shift_value << 1) ^ exclusive_or;
|
||||
}
|
||||
xor_table[c] = shift_value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the CRC to the reset value.
|
||||
void reset() { value_ = reset_value; }
|
||||
/// Resets the CRC to the reset value.
|
||||
void reset() { value_ = reset_value; }
|
||||
|
||||
/// Updates the CRC to include @c byte.
|
||||
void add(uint8_t byte) {
|
||||
if constexpr (reflect_input) byte = reverse_byte(byte);
|
||||
value_ = IntType((value_ << 8) ^ xor_table[(value_ >> multibyte_shift) ^ byte]);
|
||||
}
|
||||
/// Updates the CRC to include @c byte.
|
||||
void add(uint8_t byte) {
|
||||
if constexpr (reflect_input) byte = reverse_byte(byte);
|
||||
value_ = IntType((value_ << 8) ^ xor_table[(value_ >> multibyte_shift) ^ byte]);
|
||||
}
|
||||
|
||||
/// @returns The current value of the CRC.
|
||||
inline IntType get_value() const {
|
||||
IntType result = value_ ^ output_xor;
|
||||
if constexpr (reflect_output) {
|
||||
IntType reflected_output = 0;
|
||||
for(std::size_t c = 0; c < sizeof(IntType); ++c) {
|
||||
reflected_output = IntType(reflected_output << 8) | IntType(reverse_byte(result & 0xff));
|
||||
result >>= 8;
|
||||
}
|
||||
return reflected_output;
|
||||
/// @returns The current value of the CRC.
|
||||
inline IntType get_value() const {
|
||||
IntType result = value_ ^ output_xor;
|
||||
if constexpr (reflect_output) {
|
||||
IntType reflected_output = 0;
|
||||
for(std::size_t c = 0; c < sizeof(IntType); ++c) {
|
||||
reflected_output = IntType(reflected_output << 8) | IntType(reverse_byte(result & 0xff));
|
||||
result >>= 8;
|
||||
}
|
||||
return result;
|
||||
return reflected_output;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Sets the current value of the CRC.
|
||||
inline void set_value(IntType value) { value_ = value; }
|
||||
/// Sets the current value of the CRC.
|
||||
inline void set_value(IntType value) { value_ = value; }
|
||||
|
||||
/*!
|
||||
A compound for:
|
||||
/*!
|
||||
A compound for:
|
||||
|
||||
reset()
|
||||
[add all data from @c data]
|
||||
get_value()
|
||||
*/
|
||||
template <typename Collection> IntType compute_crc(const Collection &data) {
|
||||
return compute_crc(data.begin(), data.end());
|
||||
reset()
|
||||
[add all data from @c data]
|
||||
get_value()
|
||||
*/
|
||||
template <typename Collection> IntType compute_crc(const Collection &data) {
|
||||
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();
|
||||
}
|
||||
|
||||
/*!
|
||||
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:
|
||||
static constexpr int multibyte_shift = (sizeof(IntType) * 8) - 8;
|
||||
IntType xor_table[256];
|
||||
IntType value_;
|
||||
private:
|
||||
static constexpr int multibyte_shift = (sizeof(IntType) * 8) - 8;
|
||||
IntType xor_table[256];
|
||||
IntType value_;
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -40,41 +40,41 @@ template <> struct LSFRPolynomial<uint8_t> {
|
||||
in the specified int type.
|
||||
*/
|
||||
template <typename IntType = uint64_t, IntType polynomial = LSFRPolynomial<IntType>::value> class LFSR {
|
||||
public:
|
||||
/*!
|
||||
Constructs an LFSR with a random initial value.
|
||||
*/
|
||||
constexpr LFSR() noexcept {
|
||||
// Randomise the value, ensuring it doesn't end up being 0;
|
||||
// don't set any top bits, in case this is a signed type.
|
||||
while(!value_) {
|
||||
uint8_t *value_byte = reinterpret_cast<uint8_t *>(&value_);
|
||||
for(size_t c = 0; c < sizeof(IntType); ++c) {
|
||||
*value_byte = uint8_t(uint64_t(rand()) * 127 / RAND_MAX);
|
||||
++value_byte;
|
||||
}
|
||||
public:
|
||||
/*!
|
||||
Constructs an LFSR with a random initial value.
|
||||
*/
|
||||
constexpr LFSR() noexcept {
|
||||
// Randomise the value, ensuring it doesn't end up being 0;
|
||||
// don't set any top bits, in case this is a signed type.
|
||||
while(!value_) {
|
||||
uint8_t *value_byte = reinterpret_cast<uint8_t *>(&value_);
|
||||
for(size_t c = 0; c < sizeof(IntType); ++c) {
|
||||
*value_byte = uint8_t(uint64_t(rand()) * 127 / RAND_MAX);
|
||||
++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.
|
||||
*/
|
||||
LFSR(IntType initial_value) : value_(initial_value) {}
|
||||
An initial value of 0 is invalid.
|
||||
*/
|
||||
LFSR(IntType initial_value) : value_(initial_value) {}
|
||||
|
||||
/*!
|
||||
Advances the LSFR, returning either an @c IntType of value @c 1 or @c 0,
|
||||
determining the bit that was just shifted out.
|
||||
*/
|
||||
IntType next() {
|
||||
const auto result = IntType(value_ & 1);
|
||||
value_ = IntType((value_ >> 1) ^ (result * polynomial));
|
||||
return result;
|
||||
}
|
||||
/*!
|
||||
Advances the LSFR, returning either an @c IntType of value @c 1 or @c 0,
|
||||
determining the bit that was just shifted out.
|
||||
*/
|
||||
IntType next() {
|
||||
const auto result = IntType(value_ & 1);
|
||||
value_ = IntType((value_ >> 1) ^ (result * polynomial));
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
IntType value_ = 0;
|
||||
private:
|
||||
IntType value_ = 0;
|
||||
};
|
||||
|
||||
template <uint64_t polynomial> class LFSRv: public LFSR<typename MinIntTypeValue<polynomial>::type, polynomial> {};
|
||||
|
@ -28,47 +28,47 @@ namespace Numeric {
|
||||
/// = 65
|
||||
///
|
||||
template <int... Sizes> class NumericCoder {
|
||||
public:
|
||||
/// Modifies @c target to hold @c value at @c index.
|
||||
template <int index> static void encode(int &target, int value) {
|
||||
static_assert(index < sizeof...(Sizes), "Index must be within range");
|
||||
NumericEncoder<Sizes...>::template encode<index>(target, value);
|
||||
}
|
||||
public:
|
||||
/// Modifies @c target to hold @c value at @c index.
|
||||
template <int index> static void encode(int &target, int value) {
|
||||
static_assert(index < sizeof...(Sizes), "Index must be within range");
|
||||
NumericEncoder<Sizes...>::template encode<index>(target, value);
|
||||
}
|
||||
|
||||
/// @returns The value from @c source at @c index.
|
||||
template <int index> static int decode(int source) {
|
||||
static_assert(index < sizeof...(Sizes), "Index must be within range");
|
||||
return NumericDecoder<Sizes...>::template decode<index>(source);
|
||||
}
|
||||
/// @returns The value from @c source at @c index.
|
||||
template <int index> static int decode(int source) {
|
||||
static_assert(index < sizeof...(Sizes), "Index must be within range");
|
||||
return NumericDecoder<Sizes...>::template decode<index>(source);
|
||||
}
|
||||
|
||||
private:
|
||||
private:
|
||||
|
||||
template <int size, int... Tail>
|
||||
struct NumericEncoder {
|
||||
template <int index, int i = 0, int divider = 1> static void encode(int &target, int value) {
|
||||
if constexpr (i == index) {
|
||||
const int suffix = target % divider;
|
||||
target /= divider;
|
||||
target -= target % size;
|
||||
target += value % size;
|
||||
target *= divider;
|
||||
target += suffix;
|
||||
} else {
|
||||
NumericEncoder<Tail...>::template encode<index, i+1, divider*size>(target, value);
|
||||
}
|
||||
template <int size, int... Tail>
|
||||
struct NumericEncoder {
|
||||
template <int index, int i = 0, int divider = 1> static void encode(int &target, int value) {
|
||||
if constexpr (i == index) {
|
||||
const int suffix = target % divider;
|
||||
target /= divider;
|
||||
target -= target % size;
|
||||
target += value % size;
|
||||
target *= divider;
|
||||
target += suffix;
|
||||
} else {
|
||||
NumericEncoder<Tail...>::template encode<index, i+1, divider*size>(target, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
template <int size, int... Tail>
|
||||
struct NumericDecoder {
|
||||
template <int index, int i = 0, int divider = 1> static int decode(int source) {
|
||||
if constexpr (i == index) {
|
||||
return (source / divider) % size;
|
||||
} else {
|
||||
return NumericDecoder<Tail...>::template decode<index, i+1, divider*size>(source);
|
||||
}
|
||||
template <int size, int... Tail>
|
||||
struct NumericDecoder {
|
||||
template <int index, int i = 0, int divider = 1> static int decode(int source) {
|
||||
if constexpr (i == index) {
|
||||
return (source / divider) % size;
|
||||
} else {
|
||||
return NumericDecoder<Tail...>::template decode<index, i+1, divider*size>(source);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -55,297 +55,297 @@ class Delegate {
|
||||
colour phase for colour composite video.
|
||||
*/
|
||||
class CRT {
|
||||
private:
|
||||
// The incoming clock lengths will be multiplied by @c time_multiplier_; this increases
|
||||
// precision across the line.
|
||||
int time_multiplier_ = 1;
|
||||
private:
|
||||
// The incoming clock lengths will be multiplied by @c time_multiplier_; this increases
|
||||
// precision across the line.
|
||||
int time_multiplier_ = 1;
|
||||
|
||||
// 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
|
||||
// posted on to the scan target.
|
||||
std::unique_ptr<Flywheel> horizontal_flywheel_, vertical_flywheel_;
|
||||
int vertical_flywheel_output_divider_ = 1;
|
||||
int cycles_since_horizontal_sync_ = 0;
|
||||
Display::ScanTarget::Scan::EndPoint end_point(uint16_t data_offset);
|
||||
// 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
|
||||
// posted on to the scan target.
|
||||
std::unique_ptr<Flywheel> horizontal_flywheel_, vertical_flywheel_;
|
||||
int vertical_flywheel_output_divider_ = 1;
|
||||
int cycles_since_horizontal_sync_ = 0;
|
||||
Display::ScanTarget::Scan::EndPoint end_point(uint16_t data_offset);
|
||||
|
||||
struct Scan {
|
||||
enum Type {
|
||||
Sync, Level, Data, Blank, ColourBurst
|
||||
} type = Scan::Blank;
|
||||
int number_of_cycles = 0, number_of_samples = 0;
|
||||
uint8_t phase = 0, amplitude = 0;
|
||||
};
|
||||
void output_scan(const Scan *scan);
|
||||
struct Scan {
|
||||
enum Type {
|
||||
Sync, Level, Data, Blank, ColourBurst
|
||||
} type = Scan::Blank;
|
||||
int number_of_cycles = 0, number_of_samples = 0;
|
||||
uint8_t phase = 0, amplitude = 0;
|
||||
};
|
||||
void output_scan(const Scan *scan);
|
||||
|
||||
uint8_t colour_burst_amplitude_ = 30;
|
||||
int colour_burst_phase_adjustment_ = 0xff;
|
||||
uint8_t colour_burst_amplitude_ = 30;
|
||||
int colour_burst_phase_adjustment_ = 0xff;
|
||||
|
||||
int64_t phase_denominator_ = 1;
|
||||
int64_t phase_numerator_ = 0;
|
||||
int64_t colour_cycle_numerator_ = 1;
|
||||
bool is_alternate_line_ = false, phase_alternates_ = false, should_be_alternate_line_ = false;
|
||||
int64_t phase_denominator_ = 1;
|
||||
int64_t phase_numerator_ = 0;
|
||||
int64_t colour_cycle_numerator_ = 1;
|
||||
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);
|
||||
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);
|
||||
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_horizontal_sync_event(bool hsync_is_requested, int cycles_to_run_for, int *cycles_advanced);
|
||||
|
||||
Delegate *delegate_ = nullptr;
|
||||
int frames_since_last_delegate_call_ = 0;
|
||||
Delegate *delegate_ = nullptr;
|
||||
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_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.
|
||||
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_since_sync_ = 0; // The number of cycles since last in sync, for defeating the possibility of this being a vertical sync.
|
||||
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_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 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_per_line_ = 1;
|
||||
int cycles_per_line_ = 1;
|
||||
|
||||
Outputs::Display::ScanTarget *scan_target_ = &Outputs::Display::NullScanTarget::singleton;
|
||||
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.
|
||||
Outputs::Display::ScanTarget *scan_target_ = &Outputs::Display::NullScanTarget::singleton;
|
||||
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.
|
||||
|
||||
#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
|
||||
|
||||
public:
|
||||
/*! 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,
|
||||
is created for the machine to write raw pixel data to.
|
||||
public:
|
||||
/*! 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,
|
||||
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
|
||||
of cycles expected to take up one whole scanline of the display.
|
||||
@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.
|
||||
|
||||
@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.
|
||||
@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.
|
||||
|
||||
@param height_of_display The number of lines that nominally form one field of the display, rounded
|
||||
up to the next whole integer.
|
||||
@param height_of_display The number of lines that nominally form one field of the display, rounded
|
||||
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.
|
||||
The colour subcarrier is taken to have colour_cycle_numerator/colour_cycle_denominator cycles per line.
|
||||
@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.
|
||||
|
||||
@param vertical_sync_half_lines The expected length of vertical synchronisation (equalisation pulses aside),
|
||||
in multiples of half a line.
|
||||
@param vertical_sync_half_lines The expected length of vertical synchronisation (equalisation pulses aside),
|
||||
in multiples of half a line.
|
||||
|
||||
@param data_type The format that the caller will use for input data.
|
||||
*/
|
||||
CRT(int cycles_per_line,
|
||||
int clocks_per_pixel_greatest_common_divisor,
|
||||
int height_of_display,
|
||||
Outputs::Display::ColourSpace colour_space,
|
||||
int colour_cycle_numerator,
|
||||
int colour_cycle_denominator,
|
||||
int vertical_sync_half_lines,
|
||||
bool should_alternate,
|
||||
Outputs::Display::InputDataType data_type);
|
||||
@param data_type The format that the caller will use for input data.
|
||||
*/
|
||||
CRT(int cycles_per_line,
|
||||
int clocks_per_pixel_greatest_common_divisor,
|
||||
int height_of_display,
|
||||
Outputs::Display::ColourSpace colour_space,
|
||||
int colour_cycle_numerator,
|
||||
int colour_cycle_denominator,
|
||||
int vertical_sync_half_lines,
|
||||
bool should_alternate,
|
||||
Outputs::Display::InputDataType data_type);
|
||||
|
||||
/*! 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.
|
||||
*/
|
||||
CRT(int cycles_per_line,
|
||||
int clocks_per_pixel_greatest_common_divisor,
|
||||
int height_of_display,
|
||||
int vertical_sync_half_lines,
|
||||
Outputs::Display::InputDataType data_type);
|
||||
/*! 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.
|
||||
*/
|
||||
CRT(int cycles_per_line,
|
||||
int clocks_per_pixel_greatest_common_divisor,
|
||||
int height_of_display,
|
||||
int vertical_sync_half_lines,
|
||||
Outputs::Display::InputDataType data_type);
|
||||
|
||||
/*! Exactly identical to calling the designated constructor with colour subcarrier information
|
||||
looked up by display type.
|
||||
*/
|
||||
CRT(int cycles_per_line,
|
||||
int minimum_cycles_per_pixel,
|
||||
Outputs::Display::Type display_type,
|
||||
Outputs::Display::InputDataType data_type);
|
||||
/*! Exactly identical to calling the designated constructor with colour subcarrier information
|
||||
looked up by display type.
|
||||
*/
|
||||
CRT(int cycles_per_line,
|
||||
int minimum_cycles_per_pixel,
|
||||
Outputs::Display::Type display_type,
|
||||
Outputs::Display::InputDataType 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.
|
||||
*/
|
||||
CRT(Outputs::Display::InputDataType 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.
|
||||
*/
|
||||
CRT(Outputs::Display::InputDataType data_type);
|
||||
|
||||
/*! Resets the CRT with new timing information. The CRT then continues as though the new timing had
|
||||
been provided at construction. */
|
||||
void set_new_timing(
|
||||
int cycles_per_line,
|
||||
int height_of_display,
|
||||
Outputs::Display::ColourSpace colour_space,
|
||||
int colour_cycle_numerator,
|
||||
int colour_cycle_denominator,
|
||||
int vertical_sync_half_lines,
|
||||
bool should_alternate);
|
||||
/*! Resets the CRT with new timing information. The CRT then continues as though the new timing had
|
||||
been provided at construction. */
|
||||
void set_new_timing(
|
||||
int cycles_per_line,
|
||||
int height_of_display,
|
||||
Outputs::Display::ColourSpace colour_space,
|
||||
int colour_cycle_numerator,
|
||||
int colour_cycle_denominator,
|
||||
int vertical_sync_half_lines,
|
||||
bool should_alternate);
|
||||
|
||||
/*! 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. */
|
||||
void set_new_display_type(
|
||||
int cycles_per_line,
|
||||
Outputs::Display::Type display_type);
|
||||
/*! 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. */
|
||||
void set_new_display_type(
|
||||
int cycles_per_line,
|
||||
Outputs::Display::Type display_type);
|
||||
|
||||
/*! Changes the type of data being supplied as input.
|
||||
*/
|
||||
void set_new_data_type(Outputs::Display::InputDataType data_type);
|
||||
/*! Changes the type of data being supplied as input.
|
||||
*/
|
||||
void set_new_data_type(Outputs::Display::InputDataType data_type);
|
||||
|
||||
/*! Sets the CRT's intended aspect ratio — 4.0/3.0 by default.
|
||||
*/
|
||||
void set_aspect_ratio(float aspect_ratio);
|
||||
/*! Sets the CRT's intended aspect ratio — 4.0/3.0 by default.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
void output_sync(int number_of_cycles);
|
||||
@param number_of_cycles The amount of time to putput sync for.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
void output_blank(int number_of_cycles);
|
||||
@param number_of_cycles The amount of time to putput the blanking level for.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
void output_level(int number_of_cycles);
|
||||
@param number_of_cycles The number of cycles to repeat the output for.
|
||||
*/
|
||||
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 number_of_cycles The number of cycles to repeat the output for.
|
||||
*/
|
||||
template <typename IntT>
|
||||
void output_level(int number_of_cycles, IntT value) {
|
||||
auto colour_pointer = reinterpret_cast<IntT *>(begin_data(1));
|
||||
if(colour_pointer) *colour_pointer = value;
|
||||
output_level(number_of_cycles);
|
||||
}
|
||||
@param value The value to output.
|
||||
@param number_of_cycles The number of cycles to repeat the output for.
|
||||
*/
|
||||
template <typename IntT>
|
||||
void output_level(int number_of_cycles, IntT value) {
|
||||
auto colour_pointer = reinterpret_cast<IntT *>(begin_data(1));
|
||||
if(colour_pointer) *colour_pointer = value;
|
||||
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
|
||||
long, and that the first @c number_of_samples should be spread over @c 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
|
||||
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
|
||||
*/
|
||||
void output_data(int number_of_cycles, size_t number_of_samples);
|
||||
@see @c begin_data
|
||||
*/
|
||||
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. */
|
||||
void output_data(int number_of_cycles) {
|
||||
output_data(number_of_cycles, size_t(number_of_cycles));
|
||||
}
|
||||
/*! 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) {
|
||||
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
|
||||
per circle, e.g. 0 = 0 degrees, 128 = 180 degrees, 256 = 360 degree.
|
||||
@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.
|
||||
|
||||
@param amplitude The amplitude of the colour burst in 1/255ths of the amplitude of the
|
||||
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);
|
||||
@param amplitude The amplitude of the colour burst in 1/255ths of the amplitude of the
|
||||
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);
|
||||
|
||||
/*! 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;
|
||||
*/
|
||||
void output_default_colour_burst(int number_of_cycles, uint8_t amplitude = DefaultAmplitude);
|
||||
@param number_of_cycles The length of the colour burst;
|
||||
*/
|
||||
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
|
||||
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);
|
||||
@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.
|
||||
*/
|
||||
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
|
||||
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.
|
||||
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
|
||||
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.
|
||||
@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) {
|
||||
const auto result = scan_target_->begin_data(required_length, required_alignment);
|
||||
@param required_length The number of samples to allocate.
|
||||
@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) {
|
||||
const auto result = scan_target_->begin_data(required_length, required_alignment);
|
||||
#ifndef NDEBUG
|
||||
// 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
|
||||
// allocate on demand but may allow one failure to hold for a longer period — e.g. until the
|
||||
// next line.
|
||||
allocated_data_length_ = result ? required_length : std::numeric_limits<size_t>::max();
|
||||
// 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
|
||||
// allocate on demand but may allow one failure to hold for a longer period — e.g. until the
|
||||
// next line.
|
||||
allocated_data_length_ = result ? required_length : std::numeric_limits<size_t>::max();
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*! Sets the gamma exponent for the simulated screen. */
|
||||
void set_input_gamma(float gamma);
|
||||
/*! Sets the gamma exponent for the simulated screen. */
|
||||
void set_input_gamma(float gamma);
|
||||
|
||||
enum CompositeSourceType {
|
||||
/// The composite function provides continuous output.
|
||||
Continuous,
|
||||
/// The composite function provides discrete output with four unique values per colour cycle.
|
||||
DiscreteFourSamplesPerCycle
|
||||
};
|
||||
enum CompositeSourceType {
|
||||
/// The composite function provides continuous output.
|
||||
Continuous,
|
||||
/// The composite function provides discrete output with four unique values per colour cycle.
|
||||
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
|
||||
samples if it can exactly duplicate the sampling rate and placement of the composite sampling function.
|
||||
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.
|
||||
|
||||
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 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".
|
||||
*/
|
||||
void set_composite_function_type(CompositeSourceType type, float offset_of_first_sample = 0.0f);
|
||||
@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
|
||||
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);
|
||||
|
||||
/*! Nominates a section of the display to crop to for output. */
|
||||
void set_visible_area(Outputs::Display::Rect visible_area);
|
||||
/*! Nominates a section of the display to crop to for output. */
|
||||
void set_visible_area(Outputs::Display::Rect visible_area);
|
||||
|
||||
/*! @returns The rectangle describing a subset of the display, allowing for sync periods. */
|
||||
Outputs::Display::Rect get_rect_for_area(
|
||||
int first_line_after_sync,
|
||||
int number_of_lines,
|
||||
int first_cycle_after_sync,
|
||||
int number_of_cycles,
|
||||
float aspect_ratio) const;
|
||||
/*! @returns The rectangle describing a subset of the display, allowing for sync periods. */
|
||||
Outputs::Display::Rect get_rect_for_area(
|
||||
int first_line_after_sync,
|
||||
int number_of_lines,
|
||||
int first_cycle_after_sync,
|
||||
int number_of_cycles,
|
||||
float aspect_ratio) const;
|
||||
|
||||
/*! Sets the CRT delegate; set to @c nullptr if no delegate is desired. */
|
||||
inline void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
/*! Sets the CRT delegate; set to @c nullptr if no delegate is desired. */
|
||||
inline void set_delegate(Delegate *delegate) {
|
||||
delegate_ = delegate;
|
||||
}
|
||||
|
||||
/*! Sets the scan target for CRT output. */
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
/*! Sets the scan target for CRT output. */
|
||||
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
|
||||
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;
|
||||
/*!
|
||||
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.
|
||||
*/
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/*! Sets the display type that will be nominated to the scan target. */
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
/*! Sets the display type that will be nominated to the scan target. */
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
/*! Gets the last display type provided to set_display_type. */
|
||||
Outputs::Display::DisplayType get_display_type() const;
|
||||
/*! Gets the last display type provided to set_display_type. */
|
||||
Outputs::Display::DisplayType get_display_type() const;
|
||||
|
||||
/*! Sets the offset to apply to phase when using the PhaseLinkedLuminance8 input data type. */
|
||||
void set_phase_linked_luminance_offset(float);
|
||||
/*! Sets the offset to apply to phase when using the PhaseLinkedLuminance8 input data type. */
|
||||
void set_phase_linked_luminance_offset(float);
|
||||
|
||||
/*! Sets the input data type. */
|
||||
void set_input_data_type(Outputs::Display::InputDataType);
|
||||
/*! Sets the input data type. */
|
||||
void set_input_data_type(Outputs::Display::InputDataType);
|
||||
|
||||
/*! Sets the output brightness. */
|
||||
void set_brightness(float);
|
||||
/*! Sets the output brightness. */
|
||||
void set_brightness(float);
|
||||
};
|
||||
|
||||
/*!
|
||||
@ -353,44 +353,44 @@ class CRT {
|
||||
ask its receiver to try a different display frequency.
|
||||
*/
|
||||
template <typename Receiver> class CRTFrequencyMismatchWarner: public Outputs::CRT::Delegate {
|
||||
public:
|
||||
CRTFrequencyMismatchWarner(Receiver &receiver) : receiver_(receiver) {}
|
||||
public:
|
||||
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 {
|
||||
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_record_pointer_;
|
||||
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_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs;
|
||||
++frame_record_pointer_;
|
||||
|
||||
if(frame_record_pointer_*2 >= frame_records_.size()*3) {
|
||||
int total_number_of_frames = 0;
|
||||
int total_number_of_unexpected_vertical_syncs = 0;
|
||||
for(const auto &record: frame_records_) {
|
||||
total_number_of_frames += record.number_of_frames;
|
||||
total_number_of_unexpected_vertical_syncs += record.number_of_unexpected_vertical_syncs;
|
||||
}
|
||||
if(frame_record_pointer_*2 >= frame_records_.size()*3) {
|
||||
int total_number_of_frames = 0;
|
||||
int total_number_of_unexpected_vertical_syncs = 0;
|
||||
for(const auto &record: frame_records_) {
|
||||
total_number_of_frames += record.number_of_frames;
|
||||
total_number_of_unexpected_vertical_syncs += record.number_of_unexpected_vertical_syncs;
|
||||
}
|
||||
|
||||
if(total_number_of_unexpected_vertical_syncs >= total_number_of_frames >> 1) {
|
||||
reset();
|
||||
receiver_.register_crt_frequency_mismatch();
|
||||
}
|
||||
if(total_number_of_unexpected_vertical_syncs >= total_number_of_frames >> 1) {
|
||||
reset();
|
||||
receiver_.register_crt_frequency_mismatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
for(auto &record: frame_records_) {
|
||||
record.number_of_frames = 0;
|
||||
record.number_of_unexpected_vertical_syncs = 0;
|
||||
}
|
||||
void reset() {
|
||||
for(auto &record: frame_records_) {
|
||||
record.number_of_frames = 0;
|
||||
record.number_of_unexpected_vertical_syncs = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Receiver &receiver_;
|
||||
struct FrameRecord {
|
||||
int number_of_frames = 0;
|
||||
int number_of_unexpected_vertical_syncs = 0;
|
||||
};
|
||||
std::array<FrameRecord, 4> frame_records_;
|
||||
size_t frame_record_pointer_ = 0;
|
||||
private:
|
||||
Receiver &receiver_;
|
||||
struct FrameRecord {
|
||||
int number_of_frames = 0;
|
||||
int number_of_unexpected_vertical_syncs = 0;
|
||||
};
|
||||
std::array<FrameRecord, 4> frame_records_;
|
||||
size_t frame_record_pointer_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -229,32 +229,32 @@ struct Flywheel {
|
||||
(counter_ >= expected_next_sync_ - (standard_period_ / 100));
|
||||
}
|
||||
|
||||
private:
|
||||
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 sync_error_window_; // A constant indicating the window either side of the next expected sync in which we'll accept other syncs.
|
||||
private:
|
||||
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 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_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 counter_ = 0; // Time since the _start_ of the last sync.
|
||||
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 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_surprises_ = 0; // A count of the surprising syncs.
|
||||
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
|
||||
retrace begins and the internal counter is reset.
|
||||
For the current line, scan then occurs from [_retrace_time, _expected_next_sync), at which point
|
||||
retrace begins and the internal counter is reset.
|
||||
|
||||
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
|
||||
synchronisation. Other synchronisation events are clamped as though they occurred in that range.
|
||||
*/
|
||||
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
|
||||
synchronisation. Other synchronisation events are clamped as though they occurred in that range.
|
||||
*/
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -22,36 +22,36 @@ namespace Outputs::Display {
|
||||
to allow for host-client frame synchronisation.
|
||||
*/
|
||||
class Metrics {
|
||||
public:
|
||||
/// Notifies Metrics of a beam event.
|
||||
void announce_event(ScanTarget::Event event);
|
||||
public:
|
||||
/// Notifies Metrics of a beam event.
|
||||
void announce_event(ScanTarget::Event);
|
||||
|
||||
/// Notifies Metrics that the size of the output buffer has changed.
|
||||
void announce_did_resize();
|
||||
/// Notifies Metrics that the size of the output buffer has changed.
|
||||
void announce_did_resize();
|
||||
|
||||
/// 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);
|
||||
/// Provides Metrics with a new data point for output speed estimation.
|
||||
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.
|
||||
void announce_draw_status(bool complete);
|
||||
/// Provides Metrics with a new data point for output speed estimation, albeit without line-specific information.
|
||||
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.
|
||||
bool should_lower_resolution() const;
|
||||
/// @returns @c true if Metrics thinks a lower output buffer resolution is desirable in the abstract; @c false otherwise.
|
||||
bool should_lower_resolution() const;
|
||||
|
||||
/// @returns An estimate of the number of lines being produced per frame, excluding vertical sync.
|
||||
float visible_lines_per_frame_estimate() const;
|
||||
/// @returns An estimate of the number of lines being produced per frame, excluding vertical sync.
|
||||
float visible_lines_per_frame_estimate() const;
|
||||
|
||||
/// @returns The number of lines since vertical retrace ended.
|
||||
int current_line() const;
|
||||
/// @returns The number of lines since vertical retrace ended.
|
||||
int current_line() const;
|
||||
|
||||
private:
|
||||
int lines_this_frame_ = 0;
|
||||
std::array<int, 20> line_total_history_;
|
||||
size_t line_total_history_pointer_ = 0;
|
||||
void add_line_total(int);
|
||||
private:
|
||||
int lines_this_frame_ = 0;
|
||||
std::array<int, 20> line_total_history_;
|
||||
size_t line_total_history_pointer_ = 0;
|
||||
void add_line_total(int);
|
||||
|
||||
std::atomic<int> frames_hit_ = 0;
|
||||
std::atomic<int> frames_missed_ = 0;
|
||||
std::atomic<int> frames_hit_ = 0;
|
||||
std::atomic<int> frames_missed_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -137,31 +137,31 @@ public:
|
||||
static constexpr bool enabled = is_enabled(source);
|
||||
|
||||
struct LogLine {
|
||||
public:
|
||||
LogLine(FILE *const stream) : stream_(stream) {
|
||||
if constexpr (!enabled) return;
|
||||
public:
|
||||
LogLine(FILE *const stream) : stream_(stream) {
|
||||
if constexpr (!enabled) return;
|
||||
|
||||
const auto source_prefix = prefix(source);
|
||||
if(source_prefix) {
|
||||
fprintf(stream_, "[%s] ", source_prefix);
|
||||
}
|
||||
const auto source_prefix = prefix(source);
|
||||
if(source_prefix) {
|
||||
fprintf(stream_, "[%s] ", source_prefix);
|
||||
}
|
||||
}
|
||||
|
||||
~LogLine() {
|
||||
if constexpr (!enabled) return;
|
||||
fprintf(stream_, "\n");
|
||||
}
|
||||
~LogLine() {
|
||||
if constexpr (!enabled) return;
|
||||
fprintf(stream_, "\n");
|
||||
}
|
||||
|
||||
void append(const char *const format, ...) {
|
||||
if constexpr (!enabled) return;
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vfprintf(stream_, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
void append(const char *const format, ...) {
|
||||
if constexpr (!enabled) return;
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vfprintf(stream_, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
private:
|
||||
FILE *stream_;
|
||||
private:
|
||||
FILE *stream_;
|
||||
};
|
||||
|
||||
LogLine info() { return LogLine(stdout); }
|
||||
|
@ -18,21 +18,21 @@ namespace Outputs::Display::OpenGL {
|
||||
Provides a wrapper for drawing a solid, single-colour rectangle.
|
||||
*/
|
||||
class Rectangle {
|
||||
public:
|
||||
/*!
|
||||
Instantiates an instance of Rectange with the coordinates given.
|
||||
*/
|
||||
Rectangle(float x, float y, float width, float height);
|
||||
public:
|
||||
/*!
|
||||
Instantiates an instance of Rectange with the coordinates given.
|
||||
*/
|
||||
Rectangle(float x, float y, float width, float height);
|
||||
|
||||
/*!
|
||||
Draws this rectangle in the colour supplied.
|
||||
*/
|
||||
void draw(float red, float green, float blue);
|
||||
/*!
|
||||
Draws this rectangle in the colour supplied.
|
||||
*/
|
||||
void draw(float red, float green, float blue);
|
||||
|
||||
private:
|
||||
Shader pixel_shader_;
|
||||
GLuint drawing_vertex_array_ = 0, drawing_array_buffer_ = 0;
|
||||
GLint colour_uniform_;
|
||||
private:
|
||||
Shader pixel_shader_;
|
||||
GLuint drawing_vertex_array_ = 0, drawing_array_buffer_ = 0;
|
||||
GLint colour_uniform_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -19,70 +19,70 @@ namespace Outputs::Display::OpenGL {
|
||||
handles render-to-texture framebuffer objects.
|
||||
*/
|
||||
class TextureTarget {
|
||||
public:
|
||||
/*!
|
||||
Creates a new texture target. Contents are initially undefined.
|
||||
public:
|
||||
/*!
|
||||
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 height The height of target to create.
|
||||
@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.
|
||||
*/
|
||||
TextureTarget(GLsizei width, GLsizei height, GLenum texture_unit, GLint mag_filter, bool has_stencil_buffer);
|
||||
~TextureTarget();
|
||||
@param width The width 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 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();
|
||||
|
||||
/*!
|
||||
Binds this target as a framebuffer and sets the @c glViewport accordingly.
|
||||
*/
|
||||
void bind_framebuffer();
|
||||
/*!
|
||||
Binds this target as a framebuffer and sets the @c glViewport accordingly.
|
||||
*/
|
||||
void bind_framebuffer();
|
||||
|
||||
/*!
|
||||
Binds this target as a texture.
|
||||
*/
|
||||
void bind_texture() const;
|
||||
/*!
|
||||
Binds this target as a texture.
|
||||
*/
|
||||
void bind_texture() const;
|
||||
|
||||
/*!
|
||||
@returns the width of the texture target.
|
||||
*/
|
||||
GLsizei get_width() const {
|
||||
return width_;
|
||||
}
|
||||
/*!
|
||||
@returns the width of the texture target.
|
||||
*/
|
||||
GLsizei get_width() const {
|
||||
return width_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns the height of the texture target.
|
||||
*/
|
||||
GLsizei get_height() const {
|
||||
return height_;
|
||||
}
|
||||
/*!
|
||||
@returns the height of the texture target.
|
||||
*/
|
||||
GLsizei get_height() const {
|
||||
return height_;
|
||||
}
|
||||
|
||||
/*!
|
||||
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
|
||||
an appropriate width based on 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
|
||||
an appropriate width based on the aspect ratio.
|
||||
|
||||
@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
|
||||
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
|
||||
persistent low-value errors that can result from an IIR are hidden.
|
||||
*/
|
||||
void draw(float aspect_ratio, float colour_threshold = 0.0f) const;
|
||||
@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
|
||||
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
|
||||
persistent low-value errors that can result from an IIR are hidden.
|
||||
*/
|
||||
void draw(float aspect_ratio, float colour_threshold = 0.0f) const;
|
||||
|
||||
private:
|
||||
GLuint framebuffer_ = 0, texture_ = 0, renderbuffer_ = 0;
|
||||
const GLsizei width_ = 0, height_ = 0;
|
||||
GLsizei expanded_width_ = 0, expanded_height_ = 0;
|
||||
const GLenum texture_unit_ = 0;
|
||||
private:
|
||||
GLuint framebuffer_ = 0, texture_ = 0, renderbuffer_ = 0;
|
||||
const GLsizei width_ = 0, height_ = 0;
|
||||
GLsizei expanded_width_ = 0, expanded_height_ = 0;
|
||||
const GLenum texture_unit_ = 0;
|
||||
|
||||
mutable std::unique_ptr<Shader> pixel_shader_;
|
||||
mutable GLuint drawing_vertex_array_ = 0, drawing_array_buffer_ = 0;
|
||||
mutable float set_aspect_ratio_ = 0.0f;
|
||||
mutable std::unique_ptr<Shader> pixel_shader_;
|
||||
mutable GLuint drawing_vertex_array_ = 0, drawing_array_buffer_ = 0;
|
||||
mutable float set_aspect_ratio_ = 0.0f;
|
||||
|
||||
mutable GLint threshold_uniform_;
|
||||
mutable GLint threshold_uniform_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -36,124 +36,124 @@ namespace Outputs::Display::OpenGL {
|
||||
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?
|
||||
public:
|
||||
ScanTarget(GLuint target_framebuffer = 0, float output_gamma = 2.2f);
|
||||
~ScanTarget();
|
||||
public:
|
||||
ScanTarget(GLuint target_framebuffer = 0, float output_gamma = 2.2f);
|
||||
~ScanTarget();
|
||||
|
||||
void set_target_framebuffer(GLuint);
|
||||
void set_target_framebuffer(GLuint);
|
||||
|
||||
/*! Pushes the current state of output to the target framebuffer. */
|
||||
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. */
|
||||
void update(int output_width, int output_height);
|
||||
/*! Pushes the current state of output to the target framebuffer. */
|
||||
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. */
|
||||
void update(int output_width, int output_height);
|
||||
|
||||
private:
|
||||
static constexpr int LineBufferWidth = 2048;
|
||||
static constexpr int LineBufferHeight = 2048;
|
||||
private:
|
||||
static constexpr int LineBufferWidth = 2048;
|
||||
static constexpr int LineBufferHeight = 2048;
|
||||
|
||||
#ifndef NDEBUG
|
||||
struct OpenGLVersionDumper {
|
||||
OpenGLVersionDumper() {
|
||||
// Note the OpenGL version, as the first thing this class does prior to construction.
|
||||
Log::Logger<Log::Source::OpenGL>().info().append(
|
||||
"Constructing scan target with OpenGL %s; shading language version %s",
|
||||
glGetString(GL_VERSION),
|
||||
glGetString(GL_SHADING_LANGUAGE_VERSION));
|
||||
}
|
||||
} dumper_;
|
||||
struct OpenGLVersionDumper {
|
||||
OpenGLVersionDumper() {
|
||||
// Note the OpenGL version, as the first thing this class does prior to construction.
|
||||
Log::Logger<Log::Source::OpenGL>().info().append(
|
||||
"Constructing scan target with OpenGL %s; shading language version %s",
|
||||
glGetString(GL_VERSION),
|
||||
glGetString(GL_SHADING_LANGUAGE_VERSION));
|
||||
}
|
||||
} dumper_;
|
||||
#endif
|
||||
|
||||
GLuint target_framebuffer_;
|
||||
const float output_gamma_;
|
||||
GLuint target_framebuffer_;
|
||||
const float output_gamma_;
|
||||
|
||||
int resolution_reduction_level_ = 1;
|
||||
int output_height_ = 0;
|
||||
int resolution_reduction_level_ = 1;
|
||||
int output_height_ = 0;
|
||||
|
||||
size_t lines_submitted_ = 0;
|
||||
std::chrono::high_resolution_clock::time_point line_submission_begin_time_;
|
||||
size_t lines_submitted_ = 0;
|
||||
std::chrono::high_resolution_clock::time_point line_submission_begin_time_;
|
||||
|
||||
// Contains the first composition of scans into lines;
|
||||
// they're accumulated prior to output to allow for continuous
|
||||
// application of any necessary conversions — e.g. composite processing.
|
||||
TextureTarget unprocessed_line_texture_;
|
||||
// Contains the first composition of scans into lines;
|
||||
// they're accumulated prior to output to allow for continuous
|
||||
// application of any necessary conversions — e.g. composite processing.
|
||||
TextureTarget unprocessed_line_texture_;
|
||||
|
||||
// Contains pre-lowpass-filtered chrominance information that is
|
||||
// part-QAM-demoduled, if dealing with a QAM data source.
|
||||
std::unique_ptr<TextureTarget> qam_chroma_texture_;
|
||||
// Contains pre-lowpass-filtered chrominance information that is
|
||||
// part-QAM-demoduled, if dealing with a QAM data source.
|
||||
std::unique_ptr<TextureTarget> qam_chroma_texture_;
|
||||
|
||||
// Scans are accumulated to the accumulation texture; the full-display
|
||||
// rectangle is used to ensure untouched pixels properly decay.
|
||||
std::unique_ptr<TextureTarget> accumulation_texture_;
|
||||
Rectangle full_display_rectangle_;
|
||||
bool stencil_is_valid_ = false;
|
||||
// Scans are accumulated to the accumulation texture; the full-display
|
||||
// rectangle is used to ensure untouched pixels properly decay.
|
||||
std::unique_ptr<TextureTarget> accumulation_texture_;
|
||||
Rectangle full_display_rectangle_;
|
||||
bool stencil_is_valid_ = false;
|
||||
|
||||
// OpenGL storage handles for buffer data.
|
||||
GLuint scan_buffer_name_ = 0, scan_vertex_array_ = 0;
|
||||
GLuint line_buffer_name_ = 0, line_vertex_array_ = 0;
|
||||
// OpenGL storage handles for buffer data.
|
||||
GLuint scan_buffer_name_ = 0, scan_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 patch_buffer(const T &array, GLuint target, uint16_t submit_pointer, uint16_t read_pointer);
|
||||
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);
|
||||
|
||||
GLuint write_area_texture_name_ = 0;
|
||||
bool texture_exists_ = false;
|
||||
GLuint write_area_texture_name_ = 0;
|
||||
bool texture_exists_ = false;
|
||||
|
||||
// Receives scan target modals.
|
||||
void setup_pipeline();
|
||||
// Receives scan target modals.
|
||||
void setup_pipeline();
|
||||
|
||||
enum class ShaderType {
|
||||
Composition,
|
||||
Conversion,
|
||||
QAMSeparation
|
||||
};
|
||||
enum class ShaderType {
|
||||
Composition,
|
||||
Conversion,
|
||||
QAMSeparation
|
||||
};
|
||||
|
||||
/*!
|
||||
Calls @c taret.enable_vertex_attribute_with_pointer to attach all
|
||||
globals for shaders of @c type to @c target.
|
||||
*/
|
||||
static void enable_vertex_attributes(ShaderType type, Shader &target);
|
||||
void set_uniforms(ShaderType type, Shader &target) const;
|
||||
std::vector<std::string> bindings(ShaderType type) const;
|
||||
/*!
|
||||
Calls @c taret.enable_vertex_attribute_with_pointer to attach all
|
||||
globals for shaders of @c type to @c target.
|
||||
*/
|
||||
static void enable_vertex_attributes(ShaderType type, Shader &target);
|
||||
void set_uniforms(ShaderType type, Shader &target) const;
|
||||
std::vector<std::string> bindings(ShaderType type) const;
|
||||
|
||||
GLsync fence_ = nullptr;
|
||||
std::atomic_flag is_drawing_to_accumulation_buffer_;
|
||||
GLsync fence_ = nullptr;
|
||||
std::atomic_flag is_drawing_to_accumulation_buffer_;
|
||||
|
||||
std::unique_ptr<Shader> input_shader_;
|
||||
std::unique_ptr<Shader> output_shader_;
|
||||
std::unique_ptr<Shader> qam_separation_shader_;
|
||||
std::unique_ptr<Shader> input_shader_;
|
||||
std::unique_ptr<Shader> output_shader_;
|
||||
std::unique_ptr<Shader> qam_separation_shader_;
|
||||
|
||||
/*!
|
||||
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,
|
||||
phase-linked luminance or luminance+phase offset.
|
||||
*/
|
||||
std::unique_ptr<Shader> composition_shader() const;
|
||||
/*!
|
||||
Produces a shader that reads from a composition buffer and converts to host
|
||||
output RGB, decoding composite or S-Video as necessary.
|
||||
*/
|
||||
std::unique_ptr<Shader> conversion_shader() const;
|
||||
/*!
|
||||
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
|
||||
size of four samples per colour clock, point sampled.
|
||||
*/
|
||||
std::unique_ptr<Shader> qam_separation_shader() const;
|
||||
/*!
|
||||
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,
|
||||
phase-linked luminance or luminance+phase offset.
|
||||
*/
|
||||
std::unique_ptr<Shader> composition_shader() const;
|
||||
/*!
|
||||
Produces a shader that reads from a composition buffer and converts to host
|
||||
output RGB, decoding composite or S-Video as necessary.
|
||||
*/
|
||||
std::unique_ptr<Shader> conversion_shader() const;
|
||||
/*!
|
||||
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
|
||||
size of four samples per colour clock, point sampled.
|
||||
*/
|
||||
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
|
||||
contrast tends to be low, such as a composite colour display.
|
||||
*/
|
||||
bool is_soft_display_type();
|
||||
/*!
|
||||
@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.
|
||||
*/
|
||||
bool is_soft_display_type();
|
||||
|
||||
// Storage for the various buffers.
|
||||
std::vector<uint8_t> write_area_texture_;
|
||||
std::array<Scan, LineBufferHeight*5> scan_buffer_;
|
||||
std::array<Line, LineBufferHeight> line_buffer_;
|
||||
std::array<LineMetadata, LineBufferHeight> line_metadata_buffer_;
|
||||
// Storage for the various buffers.
|
||||
std::vector<uint8_t> write_area_texture_;
|
||||
std::array<Scan, LineBufferHeight*5> scan_buffer_;
|
||||
std::array<Line, LineBufferHeight> line_buffer_;
|
||||
std::array<LineMetadata, LineBufferHeight> line_metadata_buffer_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -33,241 +33,241 @@ namespace Outputs::Display {
|
||||
This buffer rejects new data when full.
|
||||
*/
|
||||
class BufferingScanTarget: public Outputs::Display::ScanTarget {
|
||||
public:
|
||||
/*! @returns The DisplayMetrics object that this ScanTarget has been providing with announcements and draw overages. */
|
||||
const Metrics &display_metrics();
|
||||
public:
|
||||
/*! @returns The DisplayMetrics object that this ScanTarget has been providing with announcements and draw overages. */
|
||||
const Metrics &display_metrics();
|
||||
|
||||
static constexpr int WriteAreaWidth = 2048;
|
||||
static constexpr int WriteAreaHeight = 2048;
|
||||
static constexpr int WriteAreaWidth = 2048;
|
||||
static constexpr int WriteAreaHeight = 2048;
|
||||
|
||||
BufferingScanTarget();
|
||||
BufferingScanTarget();
|
||||
|
||||
// This is included because it's assumed that scan targets will want to expose one.
|
||||
// It is the subclass's responsibility to post timings.
|
||||
Metrics display_metrics_;
|
||||
// This is included because it's assumed that scan targets will want to expose one.
|
||||
// It is the subclass's responsibility to post timings.
|
||||
Metrics display_metrics_;
|
||||
|
||||
/// Extends the definition of a Scan to include two extra fields,
|
||||
/// completing this scan's source data and destination locations.
|
||||
struct Scan {
|
||||
Outputs::Display::ScanTarget::Scan scan;
|
||||
/// Extends the definition of a Scan to include two extra fields,
|
||||
/// completing this scan's source data and destination locations.
|
||||
struct Scan {
|
||||
Outputs::Display::ScanTarget::Scan scan;
|
||||
|
||||
/// 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.
|
||||
/// Note that the data_offsets will have been adjusted to be relative to the line
|
||||
/// they fall within, not the data allocation.
|
||||
uint16_t data_y;
|
||||
/// 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
|
||||
/// this data for intermediate processing.
|
||||
uint16_t line;
|
||||
/// 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.
|
||||
/// Note that the data_offsets will have been adjusted to be relative to the line
|
||||
/// they fall within, not the data allocation.
|
||||
uint16_t data_y;
|
||||
/// 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
|
||||
/// this data for intermediate processing.
|
||||
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,
|
||||
/// 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;
|
||||
Endpoint start, end;
|
||||
|
||||
#ifndef NDEBUG
|
||||
size_t counter;
|
||||
size_t counter;
|
||||
#endif
|
||||
};
|
||||
};
|
||||
|
||||
/// Gets the current range of content that has been posted but not yet returned by
|
||||
/// a previous call to get_output_area().
|
||||
///
|
||||
/// Does not require the caller to be within a @c perform block.
|
||||
OutputArea get_output_area();
|
||||
/// Gets the current range of content that has been posted but not yet returned by
|
||||
/// a previous call to get_output_area().
|
||||
///
|
||||
/// Does not require the caller to be within a @c perform block.
|
||||
OutputArea get_output_area();
|
||||
|
||||
/// Announces that the output area has now completed output, freeing up its memory for
|
||||
/// further modification.
|
||||
///
|
||||
/// 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
|
||||
/// they were originally provided.
|
||||
///
|
||||
/// Does not require the caller to be within a @c perform block.
|
||||
void complete_output_area(const OutputArea &);
|
||||
/// Announces that the output area has now completed output, freeing up its memory for
|
||||
/// further modification.
|
||||
///
|
||||
/// 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
|
||||
/// they were originally provided.
|
||||
///
|
||||
/// Does not require the caller to be within a @c perform block.
|
||||
void complete_output_area(const OutputArea &);
|
||||
|
||||
/// Performs @c action ensuring that no other @c perform actions, or any
|
||||
/// change to modals, occurs simultaneously.
|
||||
void perform(const std::function<void(void)> &action);
|
||||
/// Performs @c action ensuring that no other @c perform actions, or any
|
||||
/// change to modals, occurs simultaneously.
|
||||
void perform(const std::function<void(void)> &action);
|
||||
|
||||
/// @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.
|
||||
const Modals *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.
|
||||
const Modals *new_modals();
|
||||
|
||||
/// @returns the current @c Modals.
|
||||
const Modals &modals() const;
|
||||
/// @returns the current @c Modals.
|
||||
const Modals &modals() const;
|
||||
|
||||
/// @returns @c true if new modals are available; @c false otherwise.
|
||||
///
|
||||
/// Safe to call from any thread.
|
||||
bool has_new_modals() const;
|
||||
/// @returns @c true if new modals are available; @c false otherwise.
|
||||
///
|
||||
/// Safe to call from any thread.
|
||||
bool has_new_modals() const;
|
||||
|
||||
private:
|
||||
// ScanTarget overrides.
|
||||
void set_modals(Modals) final;
|
||||
Outputs::Display::ScanTarget::Scan *begin_scan() final;
|
||||
void end_scan() final;
|
||||
uint8_t *begin_data(size_t required_length, size_t required_alignment) 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 will_change_owner() final;
|
||||
private:
|
||||
// ScanTarget overrides.
|
||||
void set_modals(Modals) final;
|
||||
Outputs::Display::ScanTarget::Scan *begin_scan() final;
|
||||
void end_scan() final;
|
||||
uint8_t *begin_data(size_t required_length, size_t required_alignment) 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 will_change_owner() final;
|
||||
|
||||
// Uses a texture to vend write areas.
|
||||
uint8_t *write_area_ = nullptr;
|
||||
size_t data_type_size_ = 0;
|
||||
// Uses a texture to vend write areas.
|
||||
uint8_t *write_area_ = nullptr;
|
||||
size_t data_type_size_ = 0;
|
||||
|
||||
// Tracks changes in raster visibility in order to populate
|
||||
// Lines and LineMetadatas.
|
||||
bool output_is_visible_ = false;
|
||||
// Tracks changes in raster visibility in order to populate
|
||||
// Lines and LineMetadatas.
|
||||
bool output_is_visible_ = false;
|
||||
|
||||
// Track allocation failures.
|
||||
bool data_is_allocated_ = false;
|
||||
bool allocation_has_failed_ = false;
|
||||
// Track allocation failures.
|
||||
bool data_is_allocated_ = false;
|
||||
bool allocation_has_failed_ = false;
|
||||
|
||||
// Ephemeral information for the begin/end functions.
|
||||
Scan *vended_scan_ = nullptr;
|
||||
int vended_write_area_pointer_ = 0;
|
||||
// Ephemeral information for the begin/end functions.
|
||||
Scan *vended_scan_ = nullptr;
|
||||
int vended_write_area_pointer_ = 0;
|
||||
|
||||
// Ephemeral state that helps in line composition.
|
||||
int provided_scans_ = 0;
|
||||
bool is_first_in_frame_ = true;
|
||||
bool frame_is_complete_ = true;
|
||||
bool previous_frame_was_complete_ = true;
|
||||
// Ephemeral state that helps in line composition.
|
||||
int provided_scans_ = 0;
|
||||
bool is_first_in_frame_ = true;
|
||||
bool frame_is_complete_ = true;
|
||||
bool previous_frame_was_complete_ = true;
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
struct PointerSet {
|
||||
// This constructor is here to appease GCC's interpretation of
|
||||
// an ambiguity in the C++ standard; cf. https://stackoverflow.com/questions/17430377
|
||||
PointerSet() noexcept = default;
|
||||
// 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
|
||||
// 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.
|
||||
struct PointerSet {
|
||||
// This constructor is here to appease GCC's interpretation of
|
||||
// an ambiguity in the C++ standard; cf. https://stackoverflow.com/questions/17430377
|
||||
PointerSet() noexcept = default;
|
||||
|
||||
// Squeezing this struct into 64 bits makes the std::atomics more likely
|
||||
// to be lock free; they are under LLVM x86-64.
|
||||
// Squeezing this struct into 64 bits makes the std::atomics more likely
|
||||
// to be lock free; they are under LLVM x86-64.
|
||||
|
||||
// Points to the vended area in the write area texture.
|
||||
// The vended area is always preceded by a guard pixel, so a
|
||||
// sensible default construction is write_area = 1.
|
||||
int32_t write_area = 1;
|
||||
// Points to the vended area in the write area texture.
|
||||
// The vended area is always preceded by a guard pixel, so a
|
||||
// sensible default construction is write_area = 1.
|
||||
int32_t write_area = 1;
|
||||
|
||||
// Points into the scan buffer.
|
||||
uint16_t scan = 0;
|
||||
// Points into the scan buffer.
|
||||
uint16_t scan = 0;
|
||||
|
||||
// Points into the line buffer.
|
||||
uint16_t line = 0;
|
||||
};
|
||||
// Points into the line buffer.
|
||||
uint16_t line = 0;
|
||||
};
|
||||
|
||||
/// A pointer to the final thing currently cleared for submission.
|
||||
std::atomic<PointerSet> submit_pointers_;
|
||||
/// A pointer to the final thing currently cleared for submission.
|
||||
std::atomic<PointerSet> submit_pointers_;
|
||||
|
||||
/// 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_
|
||||
/// may run and is therefore used by both producer and consumer.
|
||||
std::atomic<PointerSet> read_pointers_;
|
||||
/// 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_
|
||||
/// may run and is therefore used by both producer and consumer.
|
||||
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.
|
||||
std::atomic_flag is_updating_;
|
||||
/// This is used as a spinlock to guard `perform` calls.
|
||||
std::atomic_flag is_updating_;
|
||||
|
||||
/// 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, scans and lines.
|
||||
///
|
||||
/// This is almost never contended. The main collision is a user-prompted change of modals while the
|
||||
/// emulation thread is running.
|
||||
std::mutex producer_mutex_;
|
||||
/// 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, scans and lines.
|
||||
///
|
||||
/// This is almost never contended. The main collision is a user-prompted change of modals while the
|
||||
/// emulation thread is running.
|
||||
std::mutex producer_mutex_;
|
||||
|
||||
/// A pointer to the next thing that should be provided to the caller for data.
|
||||
PointerSet write_pointers_;
|
||||
/// A pointer to the next thing that should be provided to the caller for data.
|
||||
PointerSet write_pointers_;
|
||||
|
||||
// The owner-supplied scan buffer and size.
|
||||
Scan *scan_buffer_ = nullptr;
|
||||
size_t scan_buffer_size_ = 0;
|
||||
// The owner-supplied scan buffer and size.
|
||||
Scan *scan_buffer_ = nullptr;
|
||||
size_t scan_buffer_size_ = 0;
|
||||
|
||||
// The owner-supplied line buffer and size.
|
||||
Line *line_buffer_ = nullptr;
|
||||
LineMetadata *line_metadata_buffer_ = nullptr;
|
||||
size_t line_buffer_size_ = 0;
|
||||
// The owner-supplied line buffer and size.
|
||||
Line *line_buffer_ = nullptr;
|
||||
LineMetadata *line_metadata_buffer_ = nullptr;
|
||||
size_t line_buffer_size_ = 0;
|
||||
|
||||
// Current modals and whether they've yet been returned
|
||||
// from a call to @c get_new_modals.
|
||||
Modals modals_;
|
||||
std::atomic<bool> modals_are_dirty_ = false;
|
||||
// Current modals and whether they've yet been returned
|
||||
// from a call to @c get_new_modals.
|
||||
Modals modals_;
|
||||
std::atomic<bool> modals_are_dirty_ = false;
|
||||
|
||||
// Provides a per-data size implementation of end_data; a previous
|
||||
// implementation used blind memcpy and that turned into something
|
||||
// of a profiling hot spot.
|
||||
template <typename DataUnit> void end_data(size_t actual_length);
|
||||
// Provides a per-data size implementation of end_data; a previous
|
||||
// implementation used blind memcpy and that turned into something
|
||||
// of a profiling hot spot.
|
||||
template <typename DataUnit> void end_data(size_t actual_length);
|
||||
|
||||
#ifndef NDEBUG
|
||||
// Debug features; these amount to API validation.
|
||||
bool scan_is_ongoing_ = false;
|
||||
size_t output_area_counter_ = 0;
|
||||
size_t output_area_next_returned_ = 0;
|
||||
// Debug features; these amount to API validation.
|
||||
bool scan_is_ongoing_ = false;
|
||||
size_t output_area_counter_ = 0;
|
||||
size_t output_area_next_returned_ = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
@ -100,52 +100,52 @@ class BufferSource {
|
||||
///
|
||||
template <typename SourceT, bool stereo, int divider = 1>
|
||||
struct SampleSource: public BufferSource<SourceT, stereo> {
|
||||
public:
|
||||
template <Action action>
|
||||
void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *target) {
|
||||
auto &source = *static_cast<SourceT *>(this);
|
||||
public:
|
||||
template <Action action>
|
||||
void apply_samples(std::size_t number_of_samples, typename SampleT<stereo>::type *target) {
|
||||
auto &source = *static_cast<SourceT *>(this);
|
||||
|
||||
if constexpr (divider == 1) {
|
||||
while(number_of_samples--) {
|
||||
apply<action>(*target, source.level());
|
||||
++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_;
|
||||
}
|
||||
if constexpr (divider == 1) {
|
||||
while(number_of_samples--) {
|
||||
apply<action>(*target, source.level());
|
||||
++target;
|
||||
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.
|
||||
//
|
||||
// Until then: sample sources should implement this.
|
||||
// typename SampleT<stereo>::type level() const;
|
||||
// void advance();
|
||||
// TODO: use a concept here, when C++20 filters through.
|
||||
//
|
||||
// Until then: sample sources should implement this.
|
||||
// typename SampleT<stereo>::type level() const;
|
||||
// void advance();
|
||||
|
||||
private:
|
||||
int master_divider_{};
|
||||
private:
|
||||
int master_divider_{};
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -45,139 +45,140 @@ template <typename... S> constexpr bool are_properly_ordered() {
|
||||
An owner may optionally assign relative volumes.
|
||||
*/
|
||||
template <typename... T> class CompoundSource:
|
||||
public Outputs::Speaker::BufferSource<CompoundSource<T...>, ::Outputs::Speaker::is_stereo<T...>()> {
|
||||
private:
|
||||
template <typename... S> class CompoundSourceHolder {
|
||||
public:
|
||||
static constexpr bool is_stereo = false;
|
||||
void set_scaled_volume_range(int16_t, double *, double) {}
|
||||
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_;
|
||||
};
|
||||
public Outputs::Speaker::BufferSource<CompoundSource<T...>, ::Outputs::Speaker::is_stereo<T...>()>
|
||||
{
|
||||
private:
|
||||
template <typename... S> class CompoundSourceHolder {
|
||||
public:
|
||||
static constexpr bool is_stereo = false;
|
||||
void set_scaled_volume_range(int16_t, double *, double) {}
|
||||
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:
|
||||
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_assert(are_properly_ordered<T...>(), "Sources should be listed with all stereo sources before all mono sources");
|
||||
static constexpr bool is_stereo = S::is_stereo || CompoundSourceHolder<R...>::is_stereo;
|
||||
|
||||
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, 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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the total output volume of this CompoundSource.
|
||||
*/
|
||||
void set_sample_volume_range(int16_t range) {
|
||||
volume_range_ = range;
|
||||
push_volumes();
|
||||
static constexpr std::size_t size() {
|
||||
return 1 + CompoundSourceHolder<R...>::size();
|
||||
}
|
||||
|
||||
/*!
|
||||
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_;
|
||||
double total_scale(double *volumes) const {
|
||||
return (volumes[0] / source_.average_output_peak()) + next_source_.total_scale(&volumes[1]);
|
||||
}
|
||||
|
||||
private:
|
||||
void push_volumes() {
|
||||
const double scale = source_holder_.total_scale(volumes_.data());
|
||||
source_holder_.set_scaled_volume_range(volume_range_, volumes_.data(), scale);
|
||||
}
|
||||
S &source_;
|
||||
CompoundSourceHolder<R...> next_source_;
|
||||
std::vector<MonoSample> conversion_source_;
|
||||
};
|
||||
|
||||
CompoundSourceHolder<T...> source_holder_;
|
||||
std::vector<double> volumes_;
|
||||
int16_t volume_range_ = 0;
|
||||
std::atomic<double> average_output_peak_{1.0};
|
||||
public:
|
||||
using Sample = typename SampleT<::Outputs::Speaker::is_stereo<T...>()>::type;
|
||||
|
||||
// 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};
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -23,324 +23,324 @@
|
||||
namespace Outputs::Speaker {
|
||||
|
||||
template <typename ConcreteT, bool is_stereo> class LowpassBase: public Speaker {
|
||||
public:
|
||||
/*!
|
||||
Sets the clock rate of the input audio.
|
||||
*/
|
||||
void set_input_rate(float cycles_per_second) {
|
||||
std::lock_guard lock_guard(filter_parameters_mutex_);
|
||||
if(filter_parameters_.input_cycles_per_second == cycles_per_second) {
|
||||
return;
|
||||
}
|
||||
filter_parameters_.input_cycles_per_second = cycles_per_second;
|
||||
filter_parameters_.parameters_are_dirty = true;
|
||||
filter_parameters_.input_rate_changed = true;
|
||||
public:
|
||||
/*!
|
||||
Sets the clock rate of the input audio.
|
||||
*/
|
||||
void set_input_rate(float cycles_per_second) {
|
||||
std::lock_guard lock_guard(filter_parameters_mutex_);
|
||||
if(filter_parameters_.input_cycles_per_second == cycles_per_second) {
|
||||
return;
|
||||
}
|
||||
filter_parameters_.input_cycles_per_second = cycles_per_second;
|
||||
filter_parameters_.parameters_are_dirty = 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;
|
||||
}
|
||||
|
||||
/*!
|
||||
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;
|
||||
filter_parameters_.output_cycles_per_second = cycles_per_second;
|
||||
filter_parameters_.parameters_are_dirty = true;
|
||||
output_buffer_.resize(std::size_t(buffer_size) * (is_stereo + 1));
|
||||
}
|
||||
|
||||
// MARK: - Filtering.
|
||||
|
||||
std::size_t output_buffer_pointer_ = 0;
|
||||
std::size_t input_buffer_depth_ = 0;
|
||||
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);
|
||||
}
|
||||
|
||||
private:
|
||||
float get_ideal_clock_rate_in_range(float minimum, float maximum) final {
|
||||
std::lock_guard lock_guard(filter_parameters_mutex_);
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
step_rate_ = filter_parameters.input_cycles_per_second / filter_parameters.output_cycles_per_second;
|
||||
position_error_ = 0.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;
|
||||
filter_ = std::make_unique<SignalProcessing::FIRFilter>(
|
||||
unsigned(number_of_taps),
|
||||
filter_parameters.input_cycles_per_second,
|
||||
0.0,
|
||||
high_pass_frequency,
|
||||
SignalProcessing::FIRFilter::DefaultAttenuation);
|
||||
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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;
|
||||
|
||||
filter_parameters_.output_cycles_per_second = cycles_per_second;
|
||||
filter_parameters_.parameters_are_dirty = true;
|
||||
output_buffer_.resize(std::size_t(buffer_size) * (is_stereo + 1));
|
||||
}
|
||||
|
||||
// MARK: - Filtering.
|
||||
|
||||
std::size_t output_buffer_pointer_ = 0;
|
||||
std::size_t input_buffer_depth_ = 0;
|
||||
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);
|
||||
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;
|
||||
}
|
||||
} 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(output_buffer_.empty()) {
|
||||
return;
|
||||
}
|
||||
if constexpr (is_stereo) {
|
||||
output_buffer_[output_buffer_pointer_ + 0] = filter_->apply(input_buffer_.data(), 2);
|
||||
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) {
|
||||
output_buffer_[output_buffer_pointer_ + 0] = filter_->apply(input_buffer_.data(), 2);
|
||||
output_buffer_[output_buffer_pointer_ + 1] = filter_->apply(input_buffer_.data() + 1, 2);
|
||||
output_buffer_pointer_+= 2;
|
||||
SCALE(output_buffer_[output_buffer_pointer_ - 2]);
|
||||
SCALE(output_buffer_[output_buffer_pointer_ - 1]);
|
||||
} 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) {
|
||||
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;
|
||||
SCALE(output_buffer_[output_buffer_pointer_ - 1]);
|
||||
}
|
||||
#undef SCALE
|
||||
}
|
||||
|
||||
enum class Conversion {
|
||||
ResampleSmaller,
|
||||
Copy,
|
||||
ResampleLarger
|
||||
} 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;
|
||||
// Announce to delegate if full.
|
||||
if(output_buffer_pointer_ == output_buffer_.size()) {
|
||||
output_buffer_pointer_ = 0;
|
||||
did_complete_samples(this, output_buffer_, is_stereo);
|
||||
}
|
||||
|
||||
protected:
|
||||
bool process(size_t length) {
|
||||
const auto delegate = delegate_.load(std::memory_order_relaxed);
|
||||
if(!delegate) return false;
|
||||
|
||||
const int scale = static_cast<ConcreteT *>(this)->get_scale();
|
||||
|
||||
if(recalculate_filter_if_dirty()) {
|
||||
delegate->speaker_did_change_input_clock(this);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
enum class Conversion {
|
||||
ResampleSmaller,
|
||||
Copy,
|
||||
ResampleLarger
|
||||
} conversion_ = Conversion::Copy;
|
||||
|
||||
// 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.
|
||||
if(output_buffer_pointer_ == output_buffer_.size()) {
|
||||
output_buffer_pointer_ = 0;
|
||||
did_complete_samples(this, output_buffer_, is_stereo);
|
||||
}
|
||||
protected:
|
||||
bool process(size_t length) {
|
||||
const auto delegate = delegate_.load(std::memory_order_relaxed);
|
||||
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:
|
||||
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 -= samples_to_read;
|
||||
}
|
||||
break;
|
||||
|
||||
if(input_buffer_depth_ == input_buffer_.size()) {
|
||||
resample_input_buffer(scale);
|
||||
}
|
||||
case Conversion::ResampleSmaller:
|
||||
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:
|
||||
// TODO: input rate is less than output rate.
|
||||
break;
|
||||
}
|
||||
length -= cycles_to_read;
|
||||
}
|
||||
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.
|
||||
*/
|
||||
template <bool is_stereo> class PushLowpass: public LowpassBase<PushLowpass<is_stereo>, is_stereo> {
|
||||
private:
|
||||
using BaseT = LowpassBase<PushLowpass<is_stereo>, is_stereo>;
|
||||
friend BaseT;
|
||||
using BaseT::process;
|
||||
private:
|
||||
using BaseT = LowpassBase<PushLowpass<is_stereo>, is_stereo>;
|
||||
friend BaseT;
|
||||
using BaseT::process;
|
||||
|
||||
std::atomic<int> scale_ = 65536;
|
||||
int get_scale() const {
|
||||
return scale_.load(std::memory_order_relaxed);
|
||||
}
|
||||
std::atomic<int> scale_ = 65536;
|
||||
int get_scale() const {
|
||||
return scale_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
const int16_t *buffer_ = nullptr;
|
||||
const int16_t *buffer_ = nullptr;
|
||||
|
||||
void skip_samples(size_t count) {
|
||||
buffer_ += count;
|
||||
}
|
||||
void skip_samples(size_t count) {
|
||||
buffer_ += count;
|
||||
}
|
||||
|
||||
void get_samples(size_t length, int16_t *target) {
|
||||
const auto word_length = length * (1 + is_stereo);
|
||||
memcpy(target, buffer_, word_length * sizeof(int16_t));
|
||||
buffer_ += word_length;
|
||||
}
|
||||
void get_samples(size_t length, int16_t *target) {
|
||||
const auto word_length = length * (1 + is_stereo);
|
||||
memcpy(target, buffer_, word_length * sizeof(int16_t));
|
||||
buffer_ += word_length;
|
||||
}
|
||||
|
||||
public:
|
||||
void set_output_volume(float volume) final {
|
||||
scale_.store(int(std::clamp(volume * 65536.0f, 0.0f, 65536.0f)));
|
||||
}
|
||||
public:
|
||||
void set_output_volume(float volume) final {
|
||||
scale_.store(int(std::clamp(volume * 65536.0f, 0.0f, 65536.0f)));
|
||||
}
|
||||
|
||||
bool get_is_stereo() final {
|
||||
return is_stereo;
|
||||
}
|
||||
bool get_is_stereo() final {
|
||||
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 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
|
||||
of the number of time points at which audio was sampled.
|
||||
*/
|
||||
void push(const int16_t *buffer, size_t length) {
|
||||
buffer_ = buffer;
|
||||
@param buffer The source for samples.
|
||||
@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
|
||||
of the number of time points at which audio was sampled.
|
||||
*/
|
||||
void push(const int16_t *buffer, size_t length) {
|
||||
buffer_ = buffer;
|
||||
#ifndef NDEBUG
|
||||
const bool did_process =
|
||||
const bool did_process =
|
||||
#endif
|
||||
process(length);
|
||||
assert(!did_process || buffer_ == buffer + (length * (1 + is_stereo)));
|
||||
}
|
||||
process(length);
|
||||
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.
|
||||
*/
|
||||
template <typename SampleSource> class PullLowpass: public LowpassBase<PullLowpass<SampleSource>, SampleSource::is_stereo> {
|
||||
public:
|
||||
PullLowpass(SampleSource &sample_source) : sample_source_(sample_source) {
|
||||
// Propagate an initial volume level.
|
||||
sample_source.set_sample_volume_range(32767);
|
||||
public:
|
||||
PullLowpass(SampleSource &sample_source) : sample_source_(sample_source) {
|
||||
// Propagate an initial volume level.
|
||||
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 {
|
||||
// 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;
|
||||
}
|
||||
|
||||
queue.enqueue([this, cycles] {
|
||||
run_for(cycles);
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
using BaseT = LowpassBase<PullLowpass<SampleSource>, SampleSource::is_stereo>;
|
||||
friend BaseT;
|
||||
using BaseT::process;
|
||||
|
||||
/*!
|
||||
Advances by the number of cycles specified, 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(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);
|
||||
}
|
||||
queue.enqueue([this, cycles] {
|
||||
run_for(cycles);
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
using BaseT = LowpassBase<PullLowpass<SampleSource>, SampleSource::is_stereo>;
|
||||
friend BaseT;
|
||||
using BaseT::process;
|
||||
|
||||
/*!
|
||||
Advances by the number of cycles specified, 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(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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -61,128 +61,128 @@ template <bool stereo> struct SampleT {
|
||||
audio output.
|
||||
*/
|
||||
class Speaker {
|
||||
public:
|
||||
virtual ~Speaker() = default;
|
||||
public:
|
||||
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;
|
||||
|
||||
/*!
|
||||
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();
|
||||
// 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;
|
||||
}
|
||||
|
||||
/*!
|
||||
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;
|
||||
|
||||
/*!
|
||||
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;
|
||||
// Producer and consumer don't agree, so mix two channels to one, or double out one to two.
|
||||
if(is_stereo) {
|
||||
// Mix down.
|
||||
mix_buffer_.resize(buffer.size() / 2);
|
||||
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?
|
||||
}
|
||||
|
||||
// Producer and consumer don't agree, so mix two channels to one, or double out one to two.
|
||||
if(is_stereo) {
|
||||
// Mix down.
|
||||
mix_buffer_.resize(buffer.size() / 2);
|
||||
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];
|
||||
}
|
||||
} 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:
|
||||
void compute_output_rate() {
|
||||
// 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.
|
||||
set_computed_output_rate(output_cycles_per_second_ / input_rate_multiplier_, output_buffer_size_, stereo_output_);
|
||||
}
|
||||
private:
|
||||
void compute_output_rate() {
|
||||
// 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.
|
||||
set_computed_output_rate(output_cycles_per_second_ / input_rate_multiplier_, output_buffer_size_, stereo_output_);
|
||||
}
|
||||
|
||||
int completed_sample_sets_ = 0;
|
||||
float input_rate_multiplier_ = 1.0f;
|
||||
float output_cycles_per_second_ = 1.0f;
|
||||
int output_buffer_size_ = 1;
|
||||
std::atomic<bool> stereo_output_{false};
|
||||
std::vector<int16_t> mix_buffer_;
|
||||
int completed_sample_sets_ = 0;
|
||||
float input_rate_multiplier_ = 1.0f;
|
||||
float output_cycles_per_second_ = 1.0f;
|
||||
int output_buffer_size_ = 1;
|
||||
std::atomic<bool> stereo_output_{false};
|
||||
std::vector<int16_t> mix_buffer_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -142,28 +142,28 @@ class ProcessorBase: public ProcessorStorage {
|
||||
can produce a minor runtime performance improvement.
|
||||
*/
|
||||
template <Personality personality, typename BusHandler, bool uses_ready_line> class Processor: public ProcessorBase {
|
||||
public:
|
||||
/*!
|
||||
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) {}
|
||||
public:
|
||||
/*!
|
||||
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) {}
|
||||
|
||||
/*!
|
||||
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.
|
||||
*/
|
||||
void run_for(const Cycles cycles);
|
||||
@param cycles The number of cycles to run the 6502 for.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
void set_ready_line(bool active);
|
||||
@param active @c true if the line is logically active; @c false otherwise.
|
||||
*/
|
||||
void set_ready_line(bool active);
|
||||
|
||||
private:
|
||||
BusHandler &bus_handler_;
|
||||
private:
|
||||
BusHandler &bus_handler_;
|
||||
};
|
||||
|
||||
#include "Implementation/6502Implementation.hpp"
|
||||
|
@ -28,131 +28,131 @@ using Type = CPU::MOS6502Esque::Type;
|
||||
template <Type type, bool has_cias> class ConcreteAllRAMProcessor:
|
||||
public AllRAMProcessor, public CPU::MOS6502Esque::BusHandlerT<type>
|
||||
{
|
||||
public:
|
||||
using typename CPU::MOS6502Esque::BusHandlerT<type>::AddressType;
|
||||
public:
|
||||
using typename CPU::MOS6502Esque::BusHandlerT<type>::AddressType;
|
||||
|
||||
ConcreteAllRAMProcessor(size_t memory_size) :
|
||||
AllRAMProcessor(memory_size),
|
||||
mos6502_(*this),
|
||||
cia1_(cia1_handler_),
|
||||
cia2_(cia2_handler_) {
|
||||
mos6502_.set_power_on(false);
|
||||
ConcreteAllRAMProcessor(size_t memory_size) :
|
||||
AllRAMProcessor(memory_size),
|
||||
mos6502_(*this),
|
||||
cia1_(cia1_handler_),
|
||||
cia2_(cia2_handler_) {
|
||||
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) {
|
||||
timestamp_ += Cycles(1);
|
||||
|
||||
if constexpr (has_cias) {
|
||||
cia1_.run_for(HalfCycles(2));
|
||||
cia2_.run_for(HalfCycles(2));
|
||||
if(isAccessOperation(operation)) {
|
||||
if(operation == BusOperation::ReadOpcode) {
|
||||
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(isAccessOperation(operation)) {
|
||||
if(operation == BusOperation::ReadOpcode) {
|
||||
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)) {
|
||||
*value = memory_[address];
|
||||
|
||||
if(isReadOperation(operation)) {
|
||||
*value = memory_[address];
|
||||
|
||||
if constexpr (has_cias) {
|
||||
if((address & 0xff00) == 0xdc00) {
|
||||
*value = cia1_.read(address);
|
||||
if constexpr (LogCIAAccesses) {
|
||||
printf("[%d] CIA1: %04x -> %02x\n", timestamp_.as<int>(), address, *value);
|
||||
}
|
||||
} else if((address & 0xff00) == 0xdd00) {
|
||||
*value = cia2_.read(address);
|
||||
if constexpr (LogCIAAccesses) {
|
||||
printf("[%d] CIA2: %04x -> %02x\n", timestamp_.as<int>(), address, *value);
|
||||
}
|
||||
if constexpr (has_cias) {
|
||||
if((address & 0xff00) == 0xdc00) {
|
||||
*value = cia1_.read(address);
|
||||
if constexpr (LogCIAAccesses) {
|
||||
printf("[%d] CIA1: %04x -> %02x\n", timestamp_.as<int>(), address, *value);
|
||||
}
|
||||
} else if((address & 0xff00) == 0xdd00) {
|
||||
*value = cia2_.read(address);
|
||||
if constexpr (LogCIAAccesses) {
|
||||
printf("[%d] CIA2: %04x -> %02x\n", timestamp_.as<int>(), address, *value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (LogAllReads) {
|
||||
// if((address&0xff00) == 0x100) {
|
||||
printf("%04x -> %02x\n", address, *value);
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
memory_[address] = *value;
|
||||
if constexpr (LogAllReads) {
|
||||
// if((address&0xff00) == 0x100) {
|
||||
printf("%04x -> %02x\n", address, *value);
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
memory_[address] = *value;
|
||||
|
||||
if constexpr (has_cias) {
|
||||
if((address & 0xff00) == 0xdc00) {
|
||||
cia1_.write(address, *value);
|
||||
if constexpr (LogCIAAccesses) {
|
||||
printf("[%d] CIA1: %04x <- %02x\n", timestamp_.as<int>(), address, *value);
|
||||
}
|
||||
} else if((address & 0xff00) == 0xdd00) {
|
||||
cia2_.write(address, *value);
|
||||
if constexpr (LogCIAAccesses) {
|
||||
printf("[%d] CIA2: %04x <- %02x\n", timestamp_.as<int>(), address, *value);
|
||||
}
|
||||
if constexpr (has_cias) {
|
||||
if((address & 0xff00) == 0xdc00) {
|
||||
cia1_.write(address, *value);
|
||||
if constexpr (LogCIAAccesses) {
|
||||
printf("[%d] CIA1: %04x <- %02x\n", timestamp_.as<int>(), address, *value);
|
||||
}
|
||||
} else if((address & 0xff00) == 0xdd00) {
|
||||
cia2_.write(address, *value);
|
||||
if constexpr (LogCIAAccesses) {
|
||||
printf("[%d] CIA2: %04x <- %02x\n", timestamp_.as<int>(), address, *value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (LogAllWrites) {
|
||||
// if((address&0xff00) == 0x100) {
|
||||
printf("%04x <- %02x\n", address, *value);
|
||||
// }
|
||||
}
|
||||
if constexpr (LogAllWrites) {
|
||||
// if((address&0xff00) == 0x100) {
|
||||
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_.run_for(cycles);
|
||||
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_.run_for(cycles);
|
||||
}
|
||||
|
||||
void run_for_instructions(int count) {
|
||||
instructions_ = count;
|
||||
while(instructions_) {
|
||||
mos6502_.run_for(Cycles(1));
|
||||
}
|
||||
}
|
||||
|
||||
void run_for_instructions(int count) {
|
||||
instructions_ = count;
|
||||
while(instructions_) {
|
||||
mos6502_.run_for(Cycles(1));
|
||||
}
|
||||
}
|
||||
bool is_jammed() {
|
||||
return mos6502_.is_jammed();
|
||||
}
|
||||
|
||||
bool is_jammed() {
|
||||
return mos6502_.is_jammed();
|
||||
}
|
||||
void set_irq_line(bool value) {
|
||||
mos6502_.set_irq_line(value);
|
||||
}
|
||||
|
||||
void set_irq_line(bool value) {
|
||||
mos6502_.set_irq_line(value);
|
||||
}
|
||||
void set_nmi_line(bool value) {
|
||||
mos6502_.set_nmi_line(value);
|
||||
}
|
||||
|
||||
void set_nmi_line(bool value) {
|
||||
mos6502_.set_nmi_line(value);
|
||||
}
|
||||
uint16_t value_of(Register r) {
|
||||
return mos6502_.value_of(r);
|
||||
}
|
||||
|
||||
uint16_t value_of(Register r) {
|
||||
return mos6502_.value_of(r);
|
||||
}
|
||||
void set_value_of(Register r, uint16_t value) {
|
||||
mos6502_.set_value_of(r, value);
|
||||
}
|
||||
|
||||
void set_value_of(Register r, uint16_t value) {
|
||||
mos6502_.set_value_of(r, value);
|
||||
}
|
||||
private:
|
||||
CPU::MOS6502Esque::Processor<type, ConcreteAllRAMProcessor, false> mos6502_;
|
||||
int instructions_ = 0;
|
||||
|
||||
private:
|
||||
CPU::MOS6502Esque::Processor<type, ConcreteAllRAMProcessor, false> mos6502_;
|
||||
int instructions_ = 0;
|
||||
class PortHandler: public MOS::MOS6526::PortHandler {};
|
||||
PortHandler cia1_handler_, cia2_handler_;
|
||||
|
||||
class PortHandler: public MOS::MOS6526::PortHandler {};
|
||||
PortHandler cia1_handler_, cia2_handler_;
|
||||
|
||||
MOS::MOS6526::MOS6526<PortHandler, MOS::MOS6526::Personality::P6526> cia1_, cia2_;
|
||||
MOS::MOS6526::MOS6526<PortHandler, MOS::MOS6526::Personality::P6526> cia1_, cia2_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -34,58 +34,58 @@ enum ExtendedBusOutput {
|
||||
#include "Implementation/65816Storage.hpp"
|
||||
|
||||
class ProcessorBase: protected ProcessorStorage {
|
||||
public:
|
||||
inline void set_power_on(bool);
|
||||
inline void set_irq_line(bool);
|
||||
inline void set_nmi_line(bool);
|
||||
inline void set_reset_line(bool);
|
||||
inline void set_abort_line(bool);
|
||||
inline bool get_is_resetting() const;
|
||||
public:
|
||||
inline void set_power_on(bool);
|
||||
inline void set_irq_line(bool);
|
||||
inline void set_nmi_line(bool);
|
||||
inline void set_reset_line(bool);
|
||||
inline void set_abort_line(bool);
|
||||
inline bool get_is_resetting() const;
|
||||
|
||||
/*!
|
||||
Returns the current state of all lines not ordinarily pushed to the BusHandler,
|
||||
as listed in the ExtendedBusOutput enum.
|
||||
*/
|
||||
inline int get_extended_bus_output();
|
||||
/*!
|
||||
Returns the current state of all lines not ordinarily pushed to the BusHandler,
|
||||
as listed in the ExtendedBusOutput enum.
|
||||
*/
|
||||
inline int get_extended_bus_output();
|
||||
|
||||
/*!
|
||||
Provided for symmetry with the 6502; a 65816 is never jammed.
|
||||
*/
|
||||
inline bool is_jammed() const;
|
||||
/*!
|
||||
Provided for symmetry with the 6502; a 65816 is never jammed.
|
||||
*/
|
||||
inline bool is_jammed() const;
|
||||
|
||||
/*!
|
||||
FOR TESTING PURPOSES ONLY: forces the processor into a state where
|
||||
the next thing it intends to do is fetch a new opcode.
|
||||
*/
|
||||
inline void restart_operation_fetch();
|
||||
/*!
|
||||
FOR TESTING PURPOSES ONLY: forces the processor into a state where
|
||||
the next thing it intends to do is fetch a new opcode.
|
||||
*/
|
||||
inline void restart_operation_fetch();
|
||||
|
||||
void set_value_of(Register r, uint16_t value);
|
||||
uint16_t value_of(Register r) const;
|
||||
void set_value_of(Register r, uint16_t value);
|
||||
uint16_t value_of(Register r) const;
|
||||
};
|
||||
|
||||
template <typename BusHandler, bool uses_ready_line> class Processor: public ProcessorBase {
|
||||
public:
|
||||
/*!
|
||||
Constructs an instance of the 6502 that will use @c bus_handler for all bus communications.
|
||||
*/
|
||||
Processor(BusHandler &bus_handler) : bus_handler_(bus_handler) {}
|
||||
public:
|
||||
/*!
|
||||
Constructs an instance of the 6502 that will use @c bus_handler for all bus communications.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
void run_for(const Cycles cycles);
|
||||
@param cycles The number of cycles to run the 6502 for.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
void set_ready_line(bool active);
|
||||
@param active @c true if the line is logically active; @c false otherwise.
|
||||
*/
|
||||
void set_ready_line(bool active);
|
||||
|
||||
private:
|
||||
BusHandler &bus_handler_;
|
||||
private:
|
||||
BusHandler &bus_handler_;
|
||||
};
|
||||
|
||||
#include "Implementation/65816Implementation.hpp"
|
||||
|
@ -108,7 +108,7 @@ struct CPU::WDC65816::ProcessorStorageConstructor {
|
||||
storage_.micro_ops_.push_back(OperationDecode);
|
||||
}
|
||||
|
||||
private:
|
||||
private:
|
||||
|
||||
PatternTable::iterator install(Generator generator, AccessType access_type = AccessType::Read) {
|
||||
// Check whether this access type + addressing mode generator has already been generated.
|
||||
|
@ -395,15 +395,15 @@ struct ProcessorStorage {
|
||||
return reinterpret_cast<uint8_t *>(&value);
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t *byte(int pointer) {
|
||||
assert(pointer >= 0 && pointer < 4);
|
||||
#if TARGET_RT_BIG_ENDIAN
|
||||
return reinterpret_cast<uint8_t *>(&value) + (3 ^ pointer);
|
||||
#else
|
||||
return reinterpret_cast<uint8_t *>(&value) + pointer;
|
||||
#endif
|
||||
}
|
||||
private:
|
||||
uint8_t *byte(int pointer) {
|
||||
assert(pointer >= 0 && pointer < 4);
|
||||
#if TARGET_RT_BIG_ENDIAN
|
||||
return reinterpret_cast<uint8_t *>(&value) + (3 ^ pointer);
|
||||
#else
|
||||
return reinterpret_cast<uint8_t *>(&value) + pointer;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
Buffer instruction_buffer_, data_buffer_;
|
||||
uint32_t data_address_;
|
||||
|
@ -438,61 +438,61 @@ namespace CPU::MC68000 {
|
||||
*/
|
||||
template <class BusHandler, bool dtack_is_implicit = true, bool permit_overrun = true, bool signal_will_perform = false>
|
||||
class Processor: private ProcessorBase {
|
||||
public:
|
||||
Processor(BusHandler &bus_handler) : ProcessorBase(), bus_handler_(bus_handler) {}
|
||||
Processor(const Processor& rhs) = delete;
|
||||
Processor& operator=(const Processor& rhs) = delete;
|
||||
public:
|
||||
Processor(BusHandler &bus_handler) : ProcessorBase(), bus_handler_(bus_handler) {}
|
||||
Processor(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.
|
||||
CPU::MC68000::State get_state();
|
||||
/// @returns The current processor state.
|
||||
CPU::MC68000::State get_state();
|
||||
|
||||
/// Sets the current processor state.
|
||||
void set_state(const CPU::MC68000::State &);
|
||||
/// Sets the current processor state.
|
||||
void set_state(const CPU::MC68000::State &);
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// The queue is filled synchronously, during this call, causing calls to the bus handler.
|
||||
void decode_from_state(const InstructionSet::M68k::RegisterSet &);
|
||||
/// 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.
|
||||
///
|
||||
/// The queue is filled synchronously, during this call, causing calls to the bus handler.
|
||||
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.
|
||||
inline void set_dtack(bool dtack) {
|
||||
dtack_ = dtack;
|
||||
}
|
||||
/// Sets the DTack line — @c true for active, @c false for inactive.
|
||||
inline void set_dtack(bool dtack) {
|
||||
dtack_ = dtack;
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
vpa_ = is_peripheral_address;
|
||||
}
|
||||
/// 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) {
|
||||
vpa_ = is_peripheral_address;
|
||||
}
|
||||
|
||||
/// Sets the bus error line — @c true for active, @c false for inactive.
|
||||
inline void set_bus_error(bool bus_error) {
|
||||
berr_ = bus_error;
|
||||
}
|
||||
/// Sets the bus error line — @c true for active, @c false for inactive.
|
||||
inline void set_bus_error(bool bus_error) {
|
||||
berr_ = bus_error;
|
||||
}
|
||||
|
||||
/// Sets the interrupt lines, IPL0, IPL1 and IPL2.
|
||||
inline void set_interrupt_level(int interrupt_level) {
|
||||
bus_interrupt_level_ = interrupt_level;
|
||||
}
|
||||
/// Sets the interrupt lines, IPL0, IPL1 and IPL2.
|
||||
inline void set_interrupt_level(int interrupt_level) {
|
||||
bus_interrupt_level_ = interrupt_level;
|
||||
}
|
||||
|
||||
/// @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
|
||||
/// is into the current E cycle.
|
||||
///
|
||||
/// 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.
|
||||
HalfCycles get_e_clock_phase() {
|
||||
return e_clock_phase_;
|
||||
}
|
||||
/// @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
|
||||
/// is into the current E cycle.
|
||||
///
|
||||
/// 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.
|
||||
HalfCycles get_e_clock_phase() {
|
||||
return e_clock_phase_;
|
||||
}
|
||||
|
||||
void reset();
|
||||
void reset();
|
||||
|
||||
private:
|
||||
BusHandler &bus_handler_;
|
||||
private:
|
||||
BusHandler &bus_handler_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -17,32 +17,32 @@
|
||||
namespace CPU {
|
||||
|
||||
class AllRAMProcessor {
|
||||
public:
|
||||
AllRAMProcessor(std::size_t memory_size);
|
||||
HalfCycles get_timestamp();
|
||||
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);
|
||||
public:
|
||||
AllRAMProcessor(std::size_t memory_size);
|
||||
HalfCycles get_timestamp();
|
||||
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);
|
||||
|
||||
class TrapHandler {
|
||||
public:
|
||||
virtual void processor_did_trap(AllRAMProcessor &, uint16_t address) = 0;
|
||||
};
|
||||
void set_trap_handler(TrapHandler *trap_handler);
|
||||
void add_trap_address(uint16_t address);
|
||||
class TrapHandler {
|
||||
public:
|
||||
virtual void processor_did_trap(AllRAMProcessor &, uint16_t address) = 0;
|
||||
};
|
||||
void set_trap_handler(TrapHandler *trap_handler);
|
||||
void add_trap_address(uint16_t address);
|
||||
|
||||
protected:
|
||||
std::vector<uint8_t> memory_;
|
||||
HalfCycles timestamp_;
|
||||
protected:
|
||||
std::vector<uint8_t> memory_;
|
||||
HalfCycles timestamp_;
|
||||
|
||||
inline void check_address_for_trap(uint16_t address) {
|
||||
if(traps_[address]) {
|
||||
trap_handler_->processor_did_trap(*this, address);
|
||||
}
|
||||
inline void check_address_for_trap(uint16_t address) {
|
||||
if(traps_[address]) {
|
||||
trap_handler_->processor_did_trap(*this, address);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
TrapHandler *trap_handler_;
|
||||
std::vector<bool> traps_;
|
||||
private:
|
||||
TrapHandler *trap_handler_;
|
||||
std::vector<bool> traps_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -376,7 +376,7 @@ struct PartialMachineCycle {
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@ -386,18 +386,18 @@ struct PartialMachineCycle {
|
||||
handler.
|
||||
*/
|
||||
class BusHandler {
|
||||
public:
|
||||
/*!
|
||||
Announces that the Z80 has performed the partial machine cycle defined by @c cycle.
|
||||
public:
|
||||
/*!
|
||||
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.
|
||||
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
|
||||
via this mechanism.
|
||||
*/
|
||||
HalfCycles perform_machine_cycle([[maybe_unused]] const PartialMachineCycle &cycle) {
|
||||
return HalfCycles(0);
|
||||
}
|
||||
@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
|
||||
during some periods or may impose wait states so predictably that it's more efficient just to add them
|
||||
via this mechanism.
|
||||
*/
|
||||
HalfCycles perform_machine_cycle([[maybe_unused]] const PartialMachineCycle &) {
|
||||
return HalfCycles(0);
|
||||
}
|
||||
};
|
||||
|
||||
#include "Implementation/Z80Storage.hpp"
|
||||
@ -406,82 +406,82 @@ class BusHandler {
|
||||
A base class from which the Z80 descends; separated for implementation reasons only.
|
||||
*/
|
||||
class ProcessorBase: public ProcessorStorage {
|
||||
public:
|
||||
/*!
|
||||
Gets the value of a register.
|
||||
public:
|
||||
/*!
|
||||
Gets the value of a register.
|
||||
|
||||
@see set_value_of
|
||||
@see set_value_of
|
||||
|
||||
@param r The register to set.
|
||||
@returns The value of the register. 8-bit registers will be returned as unsigned.
|
||||
*/
|
||||
uint16_t value_of(Register r) const;
|
||||
@param r The register to set.
|
||||
@returns The value of the register. 8-bit registers will be returned as unsigned.
|
||||
*/
|
||||
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 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);
|
||||
@param r The register to set.
|
||||
@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);
|
||||
|
||||
/*!
|
||||
Gets the value of the HALT output line.
|
||||
*/
|
||||
inline bool get_halt_line() const;
|
||||
/*!
|
||||
Gets the value of the HALT output line.
|
||||
*/
|
||||
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
|
||||
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.
|
||||
*/
|
||||
inline void set_interrupt_line(bool value, HalfCycles offset = 0);
|
||||
@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
|
||||
current machine cycle. If called at any other time, this must be zero.
|
||||
*/
|
||||
inline void set_interrupt_line(bool, HalfCycles offset = 0);
|
||||
|
||||
/*!
|
||||
Gets the value of the interrupt line.
|
||||
*/
|
||||
inline bool get_interrupt_line() const;
|
||||
/*!
|
||||
Gets the value of the interrupt line.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
inline void set_non_maskable_interrupt_line(bool value, HalfCycles offset = 0);
|
||||
@param offset See discussion in set_interrupt_line.
|
||||
*/
|
||||
inline void set_non_maskable_interrupt_line(bool, HalfCycles offset = 0);
|
||||
|
||||
/*!
|
||||
Gets the value of the non-maskable interrupt line.
|
||||
*/
|
||||
inline bool get_non_maskable_interrupt_line() const;
|
||||
/*!
|
||||
Gets the value of the non-maskable interrupt line.
|
||||
*/
|
||||
inline bool get_non_maskable_interrupt_line() const;
|
||||
|
||||
/*!
|
||||
Sets the logical value of the reset line.
|
||||
*/
|
||||
inline void set_reset_line(bool value);
|
||||
/*!
|
||||
Sets the logical value of the reset line.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
bool get_is_resetting() const;
|
||||
@returns @c true if the line is logically active; @c false otherwise.
|
||||
*/
|
||||
bool get_is_resetting() const;
|
||||
|
||||
/*!
|
||||
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.
|
||||
*/
|
||||
void reset_power_on();
|
||||
/*!
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
bool is_starting_new_instruction() const;
|
||||
This is not a speedy operation.
|
||||
*/
|
||||
bool is_starting_new_instruction() const;
|
||||
};
|
||||
|
||||
/*!
|
||||
@ -493,45 +493,45 @@ class ProcessorBase: public ProcessorStorage {
|
||||
support either can produce a minor runtime performance improvement.
|
||||
*/
|
||||
template <class T, bool uses_bus_request, bool uses_wait_line> class Processor: public ProcessorBase {
|
||||
public:
|
||||
Processor(T &bus_handler);
|
||||
public:
|
||||
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.
|
||||
*/
|
||||
void run_for(const HalfCycles cycles);
|
||||
@param cycles The number of cycles to run for.
|
||||
*/
|
||||
void run_for(const HalfCycles);
|
||||
|
||||
/*!
|
||||
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);
|
||||
/*!
|
||||
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);
|
||||
|
||||
/*!
|
||||
Gets the logical value of the bus request line.
|
||||
*/
|
||||
bool get_bus_request_line() const;
|
||||
/*!
|
||||
Gets the logical value of the bus request line.
|
||||
*/
|
||||
bool get_bus_request_line() const;
|
||||
|
||||
/*!
|
||||
Sets the logical value of the wait line, having asserted that this Z80 supports the wait line.
|
||||
*/
|
||||
void set_wait_line(bool value);
|
||||
/*!
|
||||
Sets the logical value of the wait line, having asserted that this Z80 supports the wait line.
|
||||
*/
|
||||
void set_wait_line(bool);
|
||||
|
||||
/*!
|
||||
Gets the logical value of the bus request line.
|
||||
*/
|
||||
bool get_wait_line() const;
|
||||
/*!
|
||||
Gets the logical value of the bus request line.
|
||||
*/
|
||||
bool get_wait_line() const;
|
||||
|
||||
private:
|
||||
T &bus_handler_;
|
||||
private:
|
||||
T &bus_handler_;
|
||||
|
||||
void assemble_page(InstructionPage &target, InstructionTable &table, bool add_offsets);
|
||||
void copy_program(const MicroOp *source, std::vector<MicroOp> &destination);
|
||||
void assemble_page(InstructionPage &, InstructionTable &, bool add_offsets);
|
||||
void copy_program(const MicroOp *source, std::vector<MicroOp> &destination);
|
||||
};
|
||||
|
||||
#include "Implementation/Z80Implementation.hpp"
|
||||
|
Loading…
Reference in New Issue
Block a user