1
0
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:
Thomas Harte 2024-12-04 22:40:30 -05:00 committed by GitHub
commit 88b31ea940
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 5530 additions and 5528 deletions

View File

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

View File

@ -24,261 +24,261 @@ namespace Apple::II {
Implementation observation: as implemented on the IIe, the zero page setting also affects what happens in the language card area.
*/
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>();
}
};
}

View File

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

View File

@ -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 &region(uint32_t address) const { return regions_[region_map_[address >> 8]]; }
uint8_t read(const Region &region, uint32_t address) const {
return region.read ? region.read[address] : 0xff;
const Region &region(uint32_t address) const { return regions_[region_map_[address >> 8]]; }
uint8_t read(const Region &region, uint32_t address) const {
return region.read ? region.read[address] : 0xff;
}
bool is_shadowed(const Region &region, uint32_t address) const {
// ROM is never shadowed.
if(!region.write) {
return false;
}
bool is_shadowed(const Region &region, uint32_t address) const {
// 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 &region, uint32_t address, uint8_t value) {
if(!region.write) {
return;
}
// Write once.
region.write[address] = value;
// Write again, either to the same place (if unshadowed) or to the shadow destination.
static constexpr std::size_t shadow_mask[2] = {0xff'ffff, 0x01'ffff};
const bool shadowed = is_shadowed(region, address);
shadow_base_[shadowed][physical_address(region, address) & shadow_mask[shadowed]] = value;
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 &region, 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 &region, uint32_t address) const {
return std::size_t(&region.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 &region, uint32_t address) const {
return std::size_t(&region.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_{};
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,25 +38,25 @@ namespace Commodore::C1540 {
The attention input is also connected to CA1, similarly invertedl; the CA1 wire will be high when the bus is low and vice versa.
*/
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();
};
}

View File

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

View File

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

View File

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

View File

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

View File

@ -26,81 +26,82 @@ constexpr uint8_t reverse_byte(uint8_t byte) {
}
/*! Provides a class capable of generating a CRC from source data. */
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_;
};
/*!

View File

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

View File

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

View File

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

View File

@ -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.
*/
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -438,61 +438,61 @@ namespace CPU::MC68000 {
*/
template <class BusHandler, bool dtack_is_implicit = true, bool permit_overrun = true, bool signal_will_perform = false>
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_;
};
}

View File

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

View File

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