mirror of
https://github.com/TomHarte/CLK.git
synced 2025-02-27 00:30:26 +00:00
Merge pull request #1421 from TomHarte/FurtherFormatting
Further improve formatting.
This commit is contained in:
commit
bd98d95dbf
@ -74,7 +74,7 @@ uint8_t MFP68901::read(int address) {
|
||||
return 0x00;
|
||||
}
|
||||
|
||||
void MFP68901::write(int address, uint8_t value) {
|
||||
void MFP68901::write(int address, const uint8_t value) {
|
||||
address &= 0x1f;
|
||||
|
||||
// Interrupt block: enabled and masked interrupts can be set; pending and in-service interrupts can be masked.
|
||||
@ -181,7 +181,7 @@ void MFP68901::write(int address, uint8_t value) {
|
||||
}
|
||||
|
||||
template <int timer>
|
||||
void MFP68901::run_timer_for(int cycles) {
|
||||
void MFP68901::run_timer_for(const int cycles) {
|
||||
if(timers_[timer].mode >= TimerMode::Delay) {
|
||||
// This code applies the timer prescaling only. prescale_count is used to count
|
||||
// upwards rather than downwards for simplicity, but on the real hardware it's
|
||||
@ -202,7 +202,7 @@ void MFP68901::run_timer_for(int cycles) {
|
||||
}
|
||||
}
|
||||
|
||||
void MFP68901::run_for(HalfCycles time) {
|
||||
void MFP68901::run_for(const HalfCycles time) {
|
||||
cycles_left_ += time;
|
||||
|
||||
const int cycles = int(cycles_left_.flush<Cycles>().as_integral());
|
||||
@ -220,7 +220,7 @@ HalfCycles MFP68901::next_sequence_point() {
|
||||
|
||||
// MARK: - Timers
|
||||
|
||||
void MFP68901::set_timer_mode(int timer, TimerMode mode, int prescale, bool reset_timer) {
|
||||
void MFP68901::set_timer_mode(const int timer, const TimerMode mode, const int prescale, const bool reset_timer) {
|
||||
logger.error().append("Timer %d mode set: %d; prescale: %d", timer, mode, prescale);
|
||||
timers_[timer].mode = mode;
|
||||
if(reset_timer) {
|
||||
@ -236,19 +236,19 @@ void MFP68901::set_timer_mode(int timer, TimerMode mode, int prescale, bool rese
|
||||
timers_[timer].prescale = prescale;
|
||||
}
|
||||
|
||||
void MFP68901::set_timer_data(int timer, uint8_t value) {
|
||||
void MFP68901::set_timer_data(const int timer, const uint8_t value) {
|
||||
if(timers_[timer].mode == TimerMode::Stopped) {
|
||||
timers_[timer].value = value;
|
||||
}
|
||||
timers_[timer].reload_value = value;
|
||||
}
|
||||
|
||||
uint8_t MFP68901::get_timer_data(int timer) {
|
||||
uint8_t MFP68901::get_timer_data(const int timer) {
|
||||
return timers_[timer].value;
|
||||
}
|
||||
|
||||
template <int channel>
|
||||
void MFP68901::set_timer_event_input(bool value) {
|
||||
void MFP68901::set_timer_event_input(const bool value) {
|
||||
if(timers_[channel].event_input == value) return;
|
||||
|
||||
timers_[channel].event_input = value;
|
||||
@ -316,7 +316,7 @@ void MFP68901::decrement_timer(int amount) {
|
||||
}
|
||||
|
||||
// MARK: - GPIP
|
||||
void MFP68901::set_port_input(uint8_t input) {
|
||||
void MFP68901::set_port_input(const uint8_t input) {
|
||||
gpip_input_ = input;
|
||||
reevaluate_gpip_interrupts();
|
||||
}
|
||||
@ -342,12 +342,12 @@ void MFP68901::reevaluate_gpip_interrupts() {
|
||||
|
||||
// MARK: - Interrupts
|
||||
|
||||
void MFP68901::begin_interrupts(int interrupt) {
|
||||
void MFP68901::begin_interrupts(const int interrupt) {
|
||||
interrupt_pending_ |= interrupt & interrupt_enable_;
|
||||
update_interrupts();
|
||||
}
|
||||
|
||||
void MFP68901::end_interrupts(int interrupt) {
|
||||
void MFP68901::end_interrupts(const int interrupt) {
|
||||
interrupt_pending_ &= ~interrupt;
|
||||
update_interrupts();
|
||||
}
|
||||
@ -403,6 +403,6 @@ int MFP68901::acknowledge_interrupt() {
|
||||
return (interrupt_vector_ & 0xf0) | uint8_t(selected);
|
||||
}
|
||||
|
||||
void MFP68901::set_interrupt_delegate(InterruptDelegate *delegate) {
|
||||
void MFP68901::set_interrupt_delegate(InterruptDelegate *const delegate) {
|
||||
interrupt_delegate_ = delegate;
|
||||
}
|
||||
|
@ -24,162 +24,162 @@ class PortHandler {
|
||||
Models the Motorola 68901 Multi-Function Peripheral ('MFP').
|
||||
*/
|
||||
class MFP68901: public ClockingHint::Source {
|
||||
public:
|
||||
/// @returns the result of a read from @c address.
|
||||
uint8_t read(int address);
|
||||
public:
|
||||
/// @returns the result of a read from @c address.
|
||||
uint8_t read(int address);
|
||||
|
||||
/// Performs a write of @c value to @c address.
|
||||
void write(int address, uint8_t value);
|
||||
/// Performs a write of @c value to @c address.
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/// Advances the MFP by the supplied number of HalfCycles.
|
||||
void run_for(HalfCycles);
|
||||
/// Advances the MFP by the supplied number of HalfCycles.
|
||||
void run_for(HalfCycles);
|
||||
|
||||
/// @returns the number of cycles until the next possible sequence point — the next time
|
||||
/// at which the interrupt line _might_ change. This object conforms to ClockingHint::Source
|
||||
/// so that mechanism can also be used to reduce the quantity of calls into this class.
|
||||
///
|
||||
/// @discussion TODO, alas.
|
||||
HalfCycles next_sequence_point();
|
||||
/// @returns the number of cycles until the next possible sequence point — the next time
|
||||
/// at which the interrupt line _might_ change. This object conforms to ClockingHint::Source
|
||||
/// so that mechanism can also be used to reduce the quantity of calls into this class.
|
||||
///
|
||||
/// @discussion TODO, alas.
|
||||
HalfCycles next_sequence_point();
|
||||
|
||||
/// Sets the current level of either of the timer event inputs — TAI and TBI in datasheet terms.
|
||||
template <int channel>
|
||||
void set_timer_event_input(bool value);
|
||||
/// Sets the current level of either of the timer event inputs — TAI and TBI in datasheet terms.
|
||||
template <int channel>
|
||||
void set_timer_event_input(bool value);
|
||||
|
||||
/// Sets a port handler, a receiver that will be notified upon any change in GPIP output.
|
||||
///
|
||||
/// @discussion TODO.
|
||||
void set_port_handler(PortHandler *);
|
||||
/// Sets a port handler, a receiver that will be notified upon any change in GPIP output.
|
||||
///
|
||||
/// @discussion TODO.
|
||||
void set_port_handler(PortHandler *);
|
||||
|
||||
/// Sets the current input GPIP values.
|
||||
void set_port_input(uint8_t);
|
||||
/// Sets the current input GPIP values.
|
||||
void set_port_input(uint8_t);
|
||||
|
||||
/// @returns the current GPIP output values.
|
||||
///
|
||||
/// @discussion TODO.
|
||||
uint8_t get_port_output();
|
||||
/// @returns the current GPIP output values.
|
||||
///
|
||||
/// @discussion TODO.
|
||||
uint8_t get_port_output();
|
||||
|
||||
/// @returns @c true if the interrupt output is currently active; @c false otherwise.s
|
||||
bool get_interrupt_line();
|
||||
/// @returns @c true if the interrupt output is currently active; @c false otherwise.s
|
||||
bool get_interrupt_line();
|
||||
|
||||
static constexpr int NoAcknowledgement = 0x100;
|
||||
static constexpr int NoAcknowledgement = 0x100;
|
||||
|
||||
/// Communicates an interrupt acknowledge cycle.
|
||||
///
|
||||
/// @returns the vector placed on the bus if any; @c NoAcknowledgement if nothing is loaded.
|
||||
int acknowledge_interrupt();
|
||||
/// Communicates an interrupt acknowledge cycle.
|
||||
///
|
||||
/// @returns the vector placed on the bus if any; @c NoAcknowledgement if nothing is loaded.
|
||||
int acknowledge_interrupt();
|
||||
|
||||
struct InterruptDelegate {
|
||||
/// Informs the delegate of a change in the interrupt line of the nominated MFP.
|
||||
virtual void mfp68901_did_change_interrupt_status(MFP68901 *) = 0;
|
||||
};
|
||||
/// Sets a delegate that will be notified upon any change in the interrupt line.
|
||||
void set_interrupt_delegate(InterruptDelegate *delegate);
|
||||
struct InterruptDelegate {
|
||||
/// Informs the delegate of a change in the interrupt line of the nominated MFP.
|
||||
virtual void mfp68901_did_change_interrupt_status(MFP68901 *) = 0;
|
||||
};
|
||||
/// Sets a delegate that will be notified upon any change in the interrupt line.
|
||||
void set_interrupt_delegate(InterruptDelegate *);
|
||||
|
||||
// ClockingHint::Source.
|
||||
ClockingHint::Preference preferred_clocking() const final;
|
||||
// ClockingHint::Source.
|
||||
ClockingHint::Preference preferred_clocking() const final;
|
||||
|
||||
private:
|
||||
// MARK: - Timers
|
||||
enum class TimerMode {
|
||||
Stopped, EventCount, Delay, PulseWidth
|
||||
};
|
||||
void set_timer_mode(int timer, TimerMode, int prescale, bool reset_timer);
|
||||
void set_timer_data(int timer, uint8_t);
|
||||
uint8_t get_timer_data(int timer);
|
||||
template <int timer> void decrement_timer(int amount);
|
||||
template <int timer> void run_timer_for(int cycles);
|
||||
private:
|
||||
// MARK: - Timers
|
||||
enum class TimerMode {
|
||||
Stopped, EventCount, Delay, PulseWidth
|
||||
};
|
||||
void set_timer_mode(int timer, TimerMode, int prescale, bool reset_timer);
|
||||
void set_timer_data(int timer, uint8_t);
|
||||
uint8_t get_timer_data(int timer);
|
||||
template <int timer> void decrement_timer(int amount);
|
||||
template <int timer> void run_timer_for(int cycles);
|
||||
|
||||
struct Timer {
|
||||
TimerMode mode = TimerMode::Stopped;
|
||||
uint8_t value = 0;
|
||||
uint8_t reload_value = 0;
|
||||
int prescale = 1;
|
||||
int prescale_count = 1;
|
||||
bool event_input = false;
|
||||
} timers_[4];
|
||||
uint8_t timer_ab_control_[2] = { 0, 0 };
|
||||
uint8_t timer_cd_control_ = 0;
|
||||
struct Timer {
|
||||
TimerMode mode = TimerMode::Stopped;
|
||||
uint8_t value = 0;
|
||||
uint8_t reload_value = 0;
|
||||
int prescale = 1;
|
||||
int prescale_count = 1;
|
||||
bool event_input = false;
|
||||
} timers_[4];
|
||||
uint8_t timer_ab_control_[2] = { 0, 0 };
|
||||
uint8_t timer_cd_control_ = 0;
|
||||
|
||||
HalfCycles cycles_left_;
|
||||
HalfCycles cycles_left_;
|
||||
|
||||
// MARK: - GPIP
|
||||
uint8_t gpip_input_ = 0;
|
||||
uint8_t gpip_output_ = 0;
|
||||
uint8_t gpip_active_edge_ = 0;
|
||||
uint8_t gpip_direction_ = 0;
|
||||
uint8_t gpip_interrupt_state_ = 0;
|
||||
// MARK: - GPIP
|
||||
uint8_t gpip_input_ = 0;
|
||||
uint8_t gpip_output_ = 0;
|
||||
uint8_t gpip_active_edge_ = 0;
|
||||
uint8_t gpip_direction_ = 0;
|
||||
uint8_t gpip_interrupt_state_ = 0;
|
||||
|
||||
void reevaluate_gpip_interrupts();
|
||||
void reevaluate_gpip_interrupts();
|
||||
|
||||
// MARK: - Interrupts
|
||||
// MARK: - Interrupts
|
||||
|
||||
InterruptDelegate *interrupt_delegate_ = nullptr;
|
||||
InterruptDelegate *interrupt_delegate_ = nullptr;
|
||||
|
||||
// Ad hoc documentation:
|
||||
//
|
||||
// An interrupt becomes pending if it is enabled at the time it occurs.
|
||||
//
|
||||
// If a pending interrupt is enabled in the interrupt mask, a processor
|
||||
// interrupt is generated. Otherwise no processor interrupt is generated.
|
||||
//
|
||||
// (Disabling a bit in the enabled mask also instantaneously clears anything
|
||||
// in the pending mask.)
|
||||
//
|
||||
// The user can write to the pending interrupt register; a write
|
||||
// masks whatever is there — so you can disable bits but you cannot set them.
|
||||
//
|
||||
// If the vector register's 'S' bit is set then software end-of-interrupt mode applies:
|
||||
// Acknowledgement of an interrupt clears that interrupt's pending bit, but also sets
|
||||
// its in-service bit. That bit will remain set until the user writes a zero to its position.
|
||||
// If any bits are set in the in-service register, then they will prevent lower-priority
|
||||
// interrupts from being signalled to the CPU. Further interrupts of the same or a higher
|
||||
// priority may occur.
|
||||
//
|
||||
// If the vector register's 'S' bit is clear then automatic end-of-interrupt mode applies:
|
||||
// Acknowledgement of an interrupt will automatically clear the corresponding
|
||||
// pending bit.
|
||||
//
|
||||
int interrupt_enable_ = 0;
|
||||
int interrupt_pending_ = 0;
|
||||
int interrupt_mask_ = 0;
|
||||
int interrupt_in_service_ = 0;
|
||||
bool interrupt_line_ = false;
|
||||
uint8_t interrupt_vector_ = 0;
|
||||
// Ad hoc documentation:
|
||||
//
|
||||
// An interrupt becomes pending if it is enabled at the time it occurs.
|
||||
//
|
||||
// If a pending interrupt is enabled in the interrupt mask, a processor
|
||||
// interrupt is generated. Otherwise no processor interrupt is generated.
|
||||
//
|
||||
// (Disabling a bit in the enabled mask also instantaneously clears anything
|
||||
// in the pending mask.)
|
||||
//
|
||||
// The user can write to the pending interrupt register; a write
|
||||
// masks whatever is there — so you can disable bits but you cannot set them.
|
||||
//
|
||||
// If the vector register's 'S' bit is set then software end-of-interrupt mode applies:
|
||||
// Acknowledgement of an interrupt clears that interrupt's pending bit, but also sets
|
||||
// its in-service bit. That bit will remain set until the user writes a zero to its position.
|
||||
// If any bits are set in the in-service register, then they will prevent lower-priority
|
||||
// interrupts from being signalled to the CPU. Further interrupts of the same or a higher
|
||||
// priority may occur.
|
||||
//
|
||||
// If the vector register's 'S' bit is clear then automatic end-of-interrupt mode applies:
|
||||
// Acknowledgement of an interrupt will automatically clear the corresponding
|
||||
// pending bit.
|
||||
//
|
||||
int interrupt_enable_ = 0;
|
||||
int interrupt_pending_ = 0;
|
||||
int interrupt_mask_ = 0;
|
||||
int interrupt_in_service_ = 0;
|
||||
bool interrupt_line_ = false;
|
||||
uint8_t interrupt_vector_ = 0;
|
||||
|
||||
enum Interrupt {
|
||||
GPIP0 = (1 << 0),
|
||||
GPIP1 = (1 << 1),
|
||||
GPIP2 = (1 << 2),
|
||||
GPIP3 = (1 << 3),
|
||||
TimerD = (1 << 4),
|
||||
TimerC = (1 << 5),
|
||||
GPIP4 = (1 << 6),
|
||||
GPIP5 = (1 << 7),
|
||||
enum Interrupt {
|
||||
GPIP0 = (1 << 0),
|
||||
GPIP1 = (1 << 1),
|
||||
GPIP2 = (1 << 2),
|
||||
GPIP3 = (1 << 3),
|
||||
TimerD = (1 << 4),
|
||||
TimerC = (1 << 5),
|
||||
GPIP4 = (1 << 6),
|
||||
GPIP5 = (1 << 7),
|
||||
|
||||
TimerB = (1 << 8),
|
||||
TransmitError = (1 << 9),
|
||||
TransmitBufferEmpty = (1 << 10),
|
||||
ReceiveError = (1 << 11),
|
||||
ReceiveBufferFull = (1 << 12),
|
||||
TimerA = (1 << 13),
|
||||
GPIP6 = (1 << 14),
|
||||
GPIP7 = (1 << 15),
|
||||
};
|
||||
void begin_interrupts(int interrupt);
|
||||
void end_interrupts(int interrupt);
|
||||
void update_interrupts();
|
||||
TimerB = (1 << 8),
|
||||
TransmitError = (1 << 9),
|
||||
TransmitBufferEmpty = (1 << 10),
|
||||
ReceiveError = (1 << 11),
|
||||
ReceiveBufferFull = (1 << 12),
|
||||
TimerA = (1 << 13),
|
||||
GPIP6 = (1 << 14),
|
||||
GPIP7 = (1 << 15),
|
||||
};
|
||||
void begin_interrupts(int interrupt);
|
||||
void end_interrupts(int interrupt);
|
||||
void update_interrupts();
|
||||
|
||||
/// @returns the most significant bit set in v, assuming it is one of the least significant 16.
|
||||
inline static int msb16(int v) {
|
||||
// Saturate all bits below the MSB.
|
||||
v |= v >> 1;
|
||||
v |= v >> 2;
|
||||
v |= v >> 4;
|
||||
v |= v >> 8;
|
||||
/// @returns the most significant bit set in v, assuming it is one of the least significant 16.
|
||||
static constexpr int msb16(int v) {
|
||||
// Saturate all bits below the MSB.
|
||||
v |= v >> 1;
|
||||
v |= v >> 2;
|
||||
v |= v >> 4;
|
||||
v |= v >> 8;
|
||||
|
||||
// Throw away lesser bits.
|
||||
return (v+1) >> 1;
|
||||
}
|
||||
// Throw away lesser bits.
|
||||
return (v+1) >> 1;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -54,75 +54,75 @@ namespace TI::TMS {
|
||||
See get_time_until_interrupt and get_interrupt_line for asynchronous operation options.
|
||||
*/
|
||||
template <Personality personality> class TMS9918: private Base<personality> {
|
||||
public:
|
||||
/*! Constructs an instance of the VDP that behaves according to the templated personality. */
|
||||
TMS9918();
|
||||
public:
|
||||
/*! Constructs an instance of the VDP that behaves according to the templated personality. */
|
||||
TMS9918();
|
||||
|
||||
/*! Sets the TV standard for this TMS, if that is hard-coded in hardware. */
|
||||
void set_tv_standard(TVStandard standard);
|
||||
/*! Sets the TV standard for this TMS, if that is hard-coded in hardware. */
|
||||
void set_tv_standard(TVStandard);
|
||||
|
||||
/*! Sets the scan target this TMS will post content to. */
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
/*! Sets the scan target this TMS will post content to. */
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
|
||||
/*! 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;
|
||||
|
||||
/*! Sets the type of CRT display. */
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
/*! Sets the type of CRT display. */
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
/*! Gets the type of CRT display. */
|
||||
Outputs::Display::DisplayType get_display_type() const;
|
||||
/*! Gets the type of CRT display. */
|
||||
Outputs::Display::DisplayType get_display_type() const;
|
||||
|
||||
/*!
|
||||
Runs the VDP for the number of cycles indicate; the input clock rate is implicitly assumed.
|
||||
/*!
|
||||
Runs the VDP for the number of cycles indicate; the input clock rate is implicitly assumed.
|
||||
|
||||
For everything except the Mega Drive VDP:
|
||||
* the input clock rate should be 3579545 Hz, the NTSC colour clock rate.
|
||||
For everything except the Mega Drive VDP:
|
||||
* the input clock rate should be 3579545 Hz, the NTSC colour clock rate.
|
||||
|
||||
For the Mega Drive:
|
||||
* the input clock rate should be around 7.6MHz; 15/7ths of the NTSC colour
|
||||
clock rate for NTSC output and 12/7ths of the PAL colour clock rate for PAL output.
|
||||
*/
|
||||
void run_for(const HalfCycles cycles);
|
||||
For the Mega Drive:
|
||||
* the input clock rate should be around 7.6MHz; 15/7ths of the NTSC colour
|
||||
clock rate for NTSC output and 12/7ths of the PAL colour clock rate for PAL output.
|
||||
*/
|
||||
void run_for(const HalfCycles);
|
||||
|
||||
/*! Sets a register value. */
|
||||
void write(int address, uint8_t value);
|
||||
/*! Sets a register value. */
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*! Gets a register value. */
|
||||
uint8_t read(int address);
|
||||
/*! Gets a register value. */
|
||||
uint8_t read(int address);
|
||||
|
||||
/*! Gets the current scan line; provided by the Sega VDPs only. */
|
||||
uint8_t get_current_line() const;
|
||||
/*! Gets the current scan line; provided by the Sega VDPs only. */
|
||||
uint8_t get_current_line() const;
|
||||
|
||||
/*! Gets the current latched horizontal counter; provided by the Sega VDPs only. */
|
||||
uint8_t get_latched_horizontal_counter() const;
|
||||
/*! Gets the current latched horizontal counter; provided by the Sega VDPs only. */
|
||||
uint8_t get_latched_horizontal_counter() const;
|
||||
|
||||
/*! Latches the current horizontal counter. */
|
||||
void latch_horizontal_counter();
|
||||
/*! Latches the current horizontal counter. */
|
||||
void latch_horizontal_counter();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until @c get_interrupt_line would next change if
|
||||
there are no interceding calls to @c write or to @c read.
|
||||
/*!
|
||||
Returns the amount of time until @c get_interrupt_line would next change if
|
||||
there are no interceding calls to @c write or to @c read.
|
||||
|
||||
If get_interrupt_line is true now of if get_interrupt_line would
|
||||
never return true, returns HalfCycles::max().
|
||||
*/
|
||||
HalfCycles next_sequence_point() const;
|
||||
If get_interrupt_line is true now of if get_interrupt_line would
|
||||
never return true, returns HalfCycles::max().
|
||||
*/
|
||||
HalfCycles next_sequence_point() const;
|
||||
|
||||
/*!
|
||||
Returns the amount of time until the nominated line interrupt position is
|
||||
reached on line @c line. If no line interrupt position is defined for
|
||||
this VDP, returns the time until the 'beginning' of that line, whatever
|
||||
that may mean.
|
||||
/*!
|
||||
Returns the amount of time until the nominated line interrupt position is
|
||||
reached on line @c line. If no line interrupt position is defined for
|
||||
this VDP, returns the time until the 'beginning' of that line, whatever
|
||||
that may mean.
|
||||
|
||||
@line is relative to the first pixel line of the display and may be negative.
|
||||
*/
|
||||
HalfCycles get_time_until_line(int line);
|
||||
@line is relative to the first pixel line of the display and may be negative.
|
||||
*/
|
||||
HalfCycles get_time_until_line(int line);
|
||||
|
||||
/*!
|
||||
@returns @c true if the interrupt line is currently active; @c false otherwise.
|
||||
*/
|
||||
bool get_interrupt_line() const;
|
||||
/*!
|
||||
@returns @c true if the interrupt line is currently active; @c false otherwise.
|
||||
*/
|
||||
bool get_interrupt_line() const;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ TMS9918<personality>::TMS9918() {
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
void TMS9918<personality>::set_tv_standard(TVStandard standard) {
|
||||
void TMS9918<personality>::set_tv_standard(const TVStandard standard) {
|
||||
// TODO: the Yamaha is programmable on this at runtime.
|
||||
this->tv_standard_ = standard;
|
||||
switch(standard) {
|
||||
@ -104,7 +104,7 @@ void TMS9918<personality>::set_tv_standard(TVStandard standard) {
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
void TMS9918<personality>::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
void TMS9918<personality>::set_scan_target(Outputs::Display::ScanTarget *const scan_target) {
|
||||
this->crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
@ -118,7 +118,7 @@ Outputs::Display::ScanStatus TMS9918<personality>::get_scaled_scan_status() cons
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
void TMS9918<personality>::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
void TMS9918<personality>::set_display_type(const Outputs::Display::DisplayType display_type) {
|
||||
this->crt_.set_display_type(display_type);
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ void SpriteBuffer::reset_sprite_collection() {
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
void Base<personality>::posit_sprite(int sprite_number, int sprite_position, uint8_t screen_row) {
|
||||
void Base<personality>::posit_sprite(const int sprite_number, const int sprite_position, const uint8_t screen_row) {
|
||||
// Evaluation of visibility of sprite 0 is always the first step in
|
||||
// populating a sprite buffer; so use it to uncork a new one.
|
||||
if(!sprite_number) {
|
||||
@ -661,7 +661,7 @@ void TMS9918<personality>::run_for(const HalfCycles cycles) {
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
void Base<personality>::output_border(int cycles, [[maybe_unused]] uint32_t cram_dot) {
|
||||
void Base<personality>::output_border(int cycles, [[maybe_unused]] const uint32_t cram_dot) {
|
||||
cycles = from_internal<personality, Clock::CRT>(cycles);
|
||||
|
||||
uint32_t border_colour;
|
||||
@ -694,7 +694,7 @@ void Base<personality>::output_border(int cycles, [[maybe_unused]] uint32_t cram
|
||||
// MARK: - External interface.
|
||||
|
||||
template <Personality personality>
|
||||
int Base<personality>::masked_address(int address) const {
|
||||
int Base<personality>::masked_address(const int address) const {
|
||||
if constexpr (is_yamaha_vdp(personality)) {
|
||||
return address & 3;
|
||||
} else {
|
||||
@ -703,7 +703,7 @@ int Base<personality>::masked_address(int address) const {
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
void Base<personality>::write_vram(uint8_t value) {
|
||||
void Base<personality>::write_vram(const uint8_t value) {
|
||||
write_phase_ = false;
|
||||
|
||||
// Enqueue the write to occur at the next available slot.
|
||||
@ -713,7 +713,7 @@ void Base<personality>::write_vram(uint8_t value) {
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
void Base<personality>::commit_register(int reg, uint8_t value) {
|
||||
void Base<personality>::commit_register(int reg, const uint8_t value) {
|
||||
if constexpr (is_yamaha_vdp(personality)) {
|
||||
reg &= 0x3f;
|
||||
} else if constexpr (is_sega_vdp(personality)) {
|
||||
@ -1017,7 +1017,7 @@ void Base<personality>::commit_register(int reg, uint8_t value) {
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
void Base<personality>::write_register(uint8_t value) {
|
||||
void Base<personality>::write_register(const uint8_t value) {
|
||||
// Writes to address 1 are performed in pairs; if this is the
|
||||
// low byte of a value, store it and wait for the high byte.
|
||||
if(!write_phase_) {
|
||||
@ -1068,7 +1068,7 @@ void Base<personality>::write_register(uint8_t value) {
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
void Base<personality>::write_palette(uint8_t value) {
|
||||
void Base<personality>::write_palette(const uint8_t value) {
|
||||
if constexpr (is_yamaha_vdp(personality)) {
|
||||
if(!Storage<personality>::palette_write_phase_) {
|
||||
Storage<personality>::new_colour_ = value;
|
||||
@ -1091,7 +1091,7 @@ void Base<personality>::write_palette(uint8_t value) {
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
void Base<personality>::write_register_indirect([[maybe_unused]] uint8_t value) {
|
||||
void Base<personality>::write_register_indirect([[maybe_unused]] const uint8_t value) {
|
||||
if constexpr (is_yamaha_vdp(personality)) {
|
||||
// Register 17 cannot be written to indirectly.
|
||||
if(Storage<personality>::indirect_register_ != 17) {
|
||||
@ -1188,7 +1188,7 @@ uint8_t Base<personality>::read_register() {
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
uint8_t TMS9918<personality>::read(int address) {
|
||||
uint8_t TMS9918<personality>::read(const int address) {
|
||||
const int target = this->masked_address(address);
|
||||
|
||||
if(target < 2) {
|
||||
|
@ -41,7 +41,7 @@ template <Personality personality> struct Base: public Storage<personality> {
|
||||
static constexpr int output_lag = 11; // i.e. pixel output will occur 11 cycles
|
||||
// after corresponding data read.
|
||||
|
||||
static constexpr uint32_t palette_pack(uint8_t r, uint8_t g, uint8_t b) {
|
||||
static constexpr uint32_t palette_pack(const uint8_t r, const uint8_t g, const uint8_t b) {
|
||||
#if TARGET_RT_BIG_ENDIAN
|
||||
return uint32_t((r << 24) | (g << 16) | (b << 8));
|
||||
#else
|
||||
@ -85,7 +85,7 @@ template <Personality personality> struct Base: public Storage<personality> {
|
||||
/// Mutates @c target such that @c source replaces the @c length bits that currently start
|
||||
/// at bit @c shift . Subsequently ensures @c target is constrained by the
|
||||
/// applicable @c memory_mask.
|
||||
template <int shift, int length = 8> void install_field(AddressT &target, uint8_t source) {
|
||||
template <int shift, int length = 8> void install_field(AddressT &target, const uint8_t source) {
|
||||
static_assert(length > 0 && length <= 8);
|
||||
constexpr auto source_mask = (1 << length) - 1;
|
||||
constexpr auto mask = AddressT(~(source_mask << shift));
|
||||
@ -302,11 +302,11 @@ template <Personality personality> struct Base: public Storage<personality> {
|
||||
return ScreenMode::Blank;
|
||||
}
|
||||
|
||||
static AddressT rotate(AddressT address) {
|
||||
static AddressT rotate(const AddressT address) {
|
||||
return AddressT((address >> 1) | (address << 16)) & memory_mask(personality);
|
||||
}
|
||||
|
||||
AddressT command_address(Vector location, bool expansion) const {
|
||||
AddressT command_address(const Vector location, const bool expansion) const {
|
||||
if constexpr (is_yamaha_vdp(personality)) {
|
||||
switch(this->underlying_mode_) {
|
||||
default:
|
||||
@ -345,7 +345,7 @@ template <Personality personality> struct Base: public Storage<personality> {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t extract_colour(uint8_t byte, Vector location) const {
|
||||
uint8_t extract_colour(const uint8_t byte, const Vector location) const {
|
||||
switch(this->screen_mode_) {
|
||||
default:
|
||||
case ScreenMode::YamahaGraphics4: // 256 pixels @ 4bpp
|
||||
@ -360,7 +360,7 @@ template <Personality personality> struct Base: public Storage<personality> {
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<uint8_t, uint8_t> command_colour_mask(Vector location) const {
|
||||
std::pair<uint8_t, uint8_t> command_colour_mask(const Vector location) const {
|
||||
if constexpr (is_yamaha_vdp(personality)) {
|
||||
auto &context = Storage<personality>::command_context_;
|
||||
auto colour = context.latched_colour.has_value() ? context.latched_colour : context.colour;
|
||||
@ -394,7 +394,7 @@ template <Personality personality> struct Base: public Storage<personality> {
|
||||
}
|
||||
}
|
||||
|
||||
void do_external_slot(int access_column) {
|
||||
void do_external_slot(const int access_column) {
|
||||
// Don't do anything if the required time for the access to become executable
|
||||
// has yet to pass.
|
||||
if(queued_access_ == MemoryAccess::None || access_column < minimum_access_column_) {
|
||||
@ -588,7 +588,7 @@ template <Personality personality> struct Base: public Storage<personality> {
|
||||
///
|
||||
/// i.e. it provides standard glue to enter a fetch sequence at any point, while the fetches themselves are templated on the cycle
|
||||
/// at which they appear for neater expression.
|
||||
template<bool use_end, typename Fetcher> void dispatch(Fetcher &fetcher, int start, int end);
|
||||
template<bool use_end, typename Fetcher> void dispatch(Fetcher &, int start, int end);
|
||||
|
||||
// Various fetchers.
|
||||
template<bool use_end> void fetch_tms_refresh(uint8_t y, int start, int end);
|
||||
@ -616,7 +616,14 @@ template <Personality personality> struct Base: public Storage<personality> {
|
||||
template<ScreenMode mode> void draw_yamaha(uint8_t y, int start, int end);
|
||||
void draw_yamaha(uint8_t y, int start, int end);
|
||||
|
||||
template <SpriteMode mode, bool double_width> void draw_sprites(uint8_t y, int start, int end, const std::array<uint32_t, 16> &palette, int *colour_buffer = nullptr);
|
||||
template <SpriteMode mode, bool double_width>
|
||||
void draw_sprites(
|
||||
uint8_t y,
|
||||
int start,
|
||||
int end,
|
||||
const std::array<uint32_t, 16> &palette,
|
||||
int *colour_buffer = nullptr
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ enum class ScreenMode {
|
||||
YamahaGraphics2 = Graphics,
|
||||
};
|
||||
|
||||
constexpr int pixels_per_byte(ScreenMode mode) {
|
||||
constexpr int pixels_per_byte(const ScreenMode mode) {
|
||||
switch(mode) {
|
||||
default:
|
||||
case ScreenMode::Blank: return 1;
|
||||
@ -54,7 +54,7 @@ constexpr int pixels_per_byte(ScreenMode mode) {
|
||||
}
|
||||
}
|
||||
|
||||
constexpr int width(ScreenMode mode) {
|
||||
constexpr int width(const ScreenMode mode) {
|
||||
switch(mode) {
|
||||
default:
|
||||
case ScreenMode::Blank: return 0;
|
||||
@ -72,11 +72,11 @@ constexpr int width(ScreenMode mode) {
|
||||
}
|
||||
}
|
||||
|
||||
constexpr bool interleaves_banks(ScreenMode mode) {
|
||||
constexpr bool interleaves_banks(const ScreenMode mode) {
|
||||
return mode == ScreenMode::YamahaGraphics6 || mode == ScreenMode::YamahaGraphics7;
|
||||
}
|
||||
|
||||
constexpr bool is_text(ScreenMode mode) {
|
||||
constexpr bool is_text(const ScreenMode mode) {
|
||||
return mode == ScreenMode::Text || mode == ScreenMode::YamahaText80;
|
||||
}
|
||||
|
||||
|
@ -89,77 +89,77 @@ template <Personality personality, Clock head, Clock... tail> constexpr int from
|
||||
an error term be tracked.
|
||||
*/
|
||||
template <Personality personality> class ClockConverter {
|
||||
public:
|
||||
/*!
|
||||
Given that another @c source external **half-cycles** has occurred,
|
||||
indicates how many complete internal **cycles** have additionally elapsed
|
||||
since the last call to @c to_internal.
|
||||
public:
|
||||
/*!
|
||||
Given that another @c source external **half-cycles** has occurred,
|
||||
indicates how many complete internal **cycles** have additionally elapsed
|
||||
since the last call to @c to_internal.
|
||||
|
||||
E.g. for the TMS, @c source will count 456 ticks per line, and the internal clock
|
||||
runs at 342 ticks per line, so the proper conversion is to multiply by 3/4.
|
||||
*/
|
||||
int to_internal(int source) {
|
||||
switch(personality) {
|
||||
// Default behaviour is to apply a multiplication by 3/4;
|
||||
// this is correct for the TMS and Sega VDPs other than the Mega Drive.
|
||||
default: {
|
||||
const int result = source * 3 + cycles_error_;
|
||||
cycles_error_ = result & 3;
|
||||
return result >> 2;
|
||||
}
|
||||
E.g. for the TMS, @c source will count 456 ticks per line, and the internal clock
|
||||
runs at 342 ticks per line, so the proper conversion is to multiply by 3/4.
|
||||
*/
|
||||
int to_internal(const int source) {
|
||||
switch(personality) {
|
||||
// Default behaviour is to apply a multiplication by 3/4;
|
||||
// this is correct for the TMS and Sega VDPs other than the Mega Drive.
|
||||
default: {
|
||||
const int result = source * 3 + cycles_error_;
|
||||
cycles_error_ = result & 3;
|
||||
return result >> 2;
|
||||
}
|
||||
|
||||
// The two Yamaha chips have an internal clock that is four times
|
||||
// as fast as the TMS, therefore a stateless translation is possible.
|
||||
case Personality::V9938:
|
||||
case Personality::V9958:
|
||||
return source * 3;
|
||||
// The two Yamaha chips have an internal clock that is four times
|
||||
// as fast as the TMS, therefore a stateless translation is possible.
|
||||
case Personality::V9938:
|
||||
case Personality::V9958:
|
||||
return source * 3;
|
||||
|
||||
// The Mega Drive runs at 3420 master clocks per line, which is then
|
||||
// divided by 4 or 5 depending on other state. That's 7 times the
|
||||
// rate provided to the CPU; given that the input is in half-cycles
|
||||
// the proper multiplier is therefore 3.5.
|
||||
case Personality::MDVDP: {
|
||||
const int result = source * 7 + cycles_error_;
|
||||
cycles_error_ = result & 1;
|
||||
return result >> 1;
|
||||
}
|
||||
// The Mega Drive runs at 3420 master clocks per line, which is then
|
||||
// divided by 4 or 5 depending on other state. That's 7 times the
|
||||
// rate provided to the CPU; given that the input is in half-cycles
|
||||
// the proper multiplier is therefore 3.5.
|
||||
case Personality::MDVDP: {
|
||||
const int result = source * 7 + cycles_error_;
|
||||
cycles_error_ = result & 1;
|
||||
return result >> 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Provides the number of external cycles that need to begin from now in order to
|
||||
get at least @c internal_cycles into the future.
|
||||
*/
|
||||
HalfCycles half_cycles_before_internal_cycles(int internal_cycles) const {
|
||||
// Logic here correlates with multipliers as per @c to_internal.
|
||||
switch(personality) {
|
||||
default:
|
||||
// Relative to the external clock multiplied by 3, it will definitely take this
|
||||
// many cycles to complete a further (internal_cycles - 1) after the current one.
|
||||
internal_cycles = (internal_cycles - 1) << 2;
|
||||
/*!
|
||||
Provides the number of external cycles that need to begin from now in order to
|
||||
get at least @c internal_cycles into the future.
|
||||
*/
|
||||
HalfCycles half_cycles_before_internal_cycles(int internal_cycles) const {
|
||||
// Logic here correlates with multipliers as per @c to_internal.
|
||||
switch(personality) {
|
||||
default:
|
||||
// Relative to the external clock multiplied by 3, it will definitely take this
|
||||
// many cycles to complete a further (internal_cycles - 1) after the current one.
|
||||
internal_cycles = (internal_cycles - 1) << 2;
|
||||
|
||||
// It will also be necessary to complete the current one.
|
||||
internal_cycles += 4 - cycles_error_;
|
||||
// It will also be necessary to complete the current one.
|
||||
internal_cycles += 4 - cycles_error_;
|
||||
|
||||
// Round up to get the first external cycle after
|
||||
// the number of internal_cycles has elapsed.
|
||||
return HalfCycles((internal_cycles + 2) / 3);
|
||||
// Round up to get the first external cycle after
|
||||
// the number of internal_cycles has elapsed.
|
||||
return HalfCycles((internal_cycles + 2) / 3);
|
||||
|
||||
case Personality::V9938:
|
||||
case Personality::V9958:
|
||||
return HalfCycles((internal_cycles + 2) / 3);
|
||||
case Personality::V9938:
|
||||
case Personality::V9958:
|
||||
return HalfCycles((internal_cycles + 2) / 3);
|
||||
|
||||
case Personality::MDVDP:
|
||||
internal_cycles = (internal_cycles - 1) << 1;
|
||||
internal_cycles += 2 - cycles_error_;
|
||||
return HalfCycles((internal_cycles + 6) / 7);
|
||||
}
|
||||
case Personality::MDVDP:
|
||||
internal_cycles = (internal_cycles - 1) << 1;
|
||||
internal_cycles += 2 - cycles_error_;
|
||||
return HalfCycles((internal_cycles + 6) / 7);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Holds current residue in conversion from the external to
|
||||
// internal clock.
|
||||
int cycles_error_ = 0;
|
||||
private:
|
||||
// Holds current residue in conversion from the external to
|
||||
// internal clock.
|
||||
int cycles_error_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -14,7 +14,13 @@ namespace TI::TMS {
|
||||
|
||||
template <Personality personality>
|
||||
template <SpriteMode mode, bool double_width>
|
||||
void Base<personality>::draw_sprites([[maybe_unused]] uint8_t y, int start, int end, const std::array<uint32_t, 16> &palette, int *colour_buffer) {
|
||||
void Base<personality>::draw_sprites(
|
||||
[[maybe_unused]] const uint8_t y,
|
||||
const int start,
|
||||
const int end,
|
||||
const std::array<uint32_t, 16> &palette,
|
||||
int *const colour_buffer
|
||||
) {
|
||||
if(!draw_line_buffer_->sprites) {
|
||||
return;
|
||||
}
|
||||
@ -255,7 +261,7 @@ void Base<personality>::draw_sprites([[maybe_unused]] uint8_t y, int start, int
|
||||
|
||||
template <Personality personality>
|
||||
template <SpriteMode sprite_mode>
|
||||
void Base<personality>::draw_tms_character(int start, int end) {
|
||||
void Base<personality>::draw_tms_character(const int start, const int end) {
|
||||
auto &line_buffer = *draw_line_buffer_;
|
||||
|
||||
// Paint the background tiles.
|
||||
@ -304,7 +310,7 @@ void Base<personality>::draw_tms_character(int start, int end) {
|
||||
|
||||
template <Personality personality>
|
||||
template <bool apply_blink>
|
||||
void Base<personality>::draw_tms_text(int start, int end) {
|
||||
void Base<personality>::draw_tms_text(const int start, const int end) {
|
||||
auto &line_buffer = *draw_line_buffer_;
|
||||
uint32_t colours[2][2] = {
|
||||
{palette()[background_colour_], palette()[text_colour_]},
|
||||
@ -345,7 +351,11 @@ void Base<personality>::draw_tms_text(int start, int end) {
|
||||
// MARK: - Master System
|
||||
|
||||
template <Personality personality>
|
||||
void Base<personality>::draw_sms([[maybe_unused]] int start, [[maybe_unused]] int end, [[maybe_unused]] uint32_t cram_dot) {
|
||||
void Base<personality>::draw_sms(
|
||||
[[maybe_unused]] const int start,
|
||||
[[maybe_unused]] const int end,
|
||||
[[maybe_unused]] const uint32_t cram_dot
|
||||
) {
|
||||
if constexpr (is_sega_vdp(personality)) {
|
||||
int colour_buffer[256];
|
||||
auto &line_buffer = *draw_line_buffer_;
|
||||
@ -449,7 +459,7 @@ void Base<personality>::draw_sms([[maybe_unused]] int start, [[maybe_unused]] in
|
||||
|
||||
template <Personality personality>
|
||||
template <ScreenMode mode>
|
||||
void Base<personality>::draw_yamaha(uint8_t y, int start, int end) {
|
||||
void Base<personality>::draw_yamaha(const uint8_t y, int start, int end) {
|
||||
[[maybe_unused]] const auto active_palette = palette();
|
||||
const int sprite_start = start >> 2;
|
||||
const int sprite_end = end >> 2;
|
||||
@ -536,7 +546,7 @@ void Base<personality>::draw_yamaha(uint8_t y, int start, int end) {
|
||||
}
|
||||
|
||||
template <Personality personality>
|
||||
void Base<personality>::draw_yamaha(uint8_t y, int start, int end) {
|
||||
void Base<personality>::draw_yamaha(const uint8_t y, const int start, const int end) {
|
||||
if constexpr (is_yamaha_vdp(personality)) {
|
||||
switch(draw_line_buffer_->screen_mode) {
|
||||
// Modes that are the same (or close enough) to those on the TMS.
|
||||
|
@ -57,23 +57,40 @@ template<bool use_end, typename SequencerT> void Base<personality>::dispatch(Seq
|
||||
|
||||
switch(start) {
|
||||
default: assert(false);
|
||||
index(0); index(1); index(2); index(3); index(4); index(5); index(6); index(7); index(8); index(9);
|
||||
index(10); index(11); index(12); index(13); index(14); index(15); index(16); index(17); index(18); index(19);
|
||||
index(20); index(21); index(22); index(23); index(24); index(25); index(26); index(27); index(28); index(29);
|
||||
index(30); index(31); index(32); index(33); index(34); index(35); index(36); index(37); index(38); index(39);
|
||||
index(40); index(41); index(42); index(43); index(44); index(45); index(46); index(47); index(48); index(49);
|
||||
index(50); index(51); index(52); index(53); index(54); index(55); index(56); index(57); index(58); index(59);
|
||||
index(60); index(61); index(62); index(63); index(64); index(65); index(66); index(67); index(68); index(69);
|
||||
index(70); index(71); index(72); index(73); index(74); index(75); index(76); index(77); index(78); index(79);
|
||||
index(80); index(81); index(82); index(83); index(84); index(85); index(86); index(87); index(88); index(89);
|
||||
index(90); index(91); index(92); index(93); index(94); index(95); index(96); index(97); index(98); index(99);
|
||||
index(100); index(101); index(102); index(103); index(104); index(105); index(106); index(107); index(108); index(109);
|
||||
index(110); index(111); index(112); index(113); index(114); index(115); index(116); index(117); index(118); index(119);
|
||||
index(120); index(121); index(122); index(123); index(124); index(125); index(126); index(127); index(128); index(129);
|
||||
index(130); index(131); index(132); index(133); index(134); index(135); index(136); index(137); index(138); index(139);
|
||||
index(140); index(141); index(142); index(143); index(144); index(145); index(146); index(147); index(148); index(149);
|
||||
index(150); index(151); index(152); index(153); index(154); index(155); index(156); index(157); index(158); index(159);
|
||||
index(160); index(161); index(162); index(163); index(164); index(165); index(166); index(167); index(168); index(169);
|
||||
index(0); index(1); index(2); index(3); index(4);
|
||||
index(5); index(6); index(7); index(8); index(9);
|
||||
index(10); index(11); index(12); index(13); index(14);
|
||||
index(15); index(16); index(17); index(18); index(19);
|
||||
index(20); index(21); index(22); index(23); index(24);
|
||||
index(25); index(26); index(27); index(28); index(29);
|
||||
index(30); index(31); index(32); index(33); index(34);
|
||||
index(35); index(36); index(37); index(38); index(39);
|
||||
index(40); index(41); index(42); index(43); index(44);
|
||||
index(45); index(46); index(47); index(48); index(49);
|
||||
index(50); index(51); index(52); index(53); index(54);
|
||||
index(55); index(56); index(57); index(58); index(59);
|
||||
index(60); index(61); index(62); index(63); index(64);
|
||||
index(65); index(66); index(67); index(68); index(69);
|
||||
index(70); index(71); index(72); index(73); index(74);
|
||||
index(75); index(76); index(77); index(78); index(79);
|
||||
index(80); index(81); index(82); index(83); index(84);
|
||||
index(85); index(86); index(87); index(88); index(89);
|
||||
index(90); index(91); index(92); index(93); index(94);
|
||||
index(95); index(96); index(97); index(98); index(99);
|
||||
index(100); index(101); index(102); index(103); index(104);
|
||||
index(105); index(106); index(107); index(108); index(109);
|
||||
index(110); index(111); index(112); index(113); index(114);
|
||||
index(115); index(116); index(117); index(118); index(119);
|
||||
index(120); index(121); index(122); index(123); index(124);
|
||||
index(125); index(126); index(127); index(128); index(129);
|
||||
index(130); index(131); index(132); index(133); index(134);
|
||||
index(135); index(136); index(137); index(138); index(139);
|
||||
index(140); index(141); index(142); index(143); index(144);
|
||||
index(145); index(146); index(147); index(148); index(149);
|
||||
index(150); index(151); index(152); index(153); index(154);
|
||||
index(155); index(156); index(157); index(158); index(159);
|
||||
index(160); index(161); index(162); index(163); index(164);
|
||||
index(165); index(166); index(167); index(168); index(169);
|
||||
index(170);
|
||||
}
|
||||
|
||||
@ -108,7 +125,7 @@ template <Personality personality>
|
||||
struct CharacterFetcher {
|
||||
using AddressT = typename Base<personality>::AddressT;
|
||||
|
||||
CharacterFetcher(Base<personality> *base, uint8_t y) :
|
||||
CharacterFetcher(Base<personality> *const base, const uint8_t y) :
|
||||
base(base),
|
||||
y(y),
|
||||
row_base(base->pattern_name_address_ & bits<10>(AddressT((y << 2)&~31)))
|
||||
@ -137,15 +154,15 @@ struct CharacterFetcher {
|
||||
}
|
||||
}
|
||||
|
||||
void fetch_name(int column) {
|
||||
void fetch_name(const int column) {
|
||||
base->tile_offset_ = base->ram_[row_base + AddressT(column)];
|
||||
}
|
||||
|
||||
void fetch_pattern(int column) {
|
||||
void fetch_pattern(const int column) {
|
||||
base->fetch_line_buffer_->tiles.patterns[column][0] = base->ram_[pattern_base + AddressT(base->tile_offset_ << 3)];
|
||||
}
|
||||
|
||||
void fetch_colour(int column) {
|
||||
void fetch_colour(const int column) {
|
||||
base->fetch_line_buffer_->tiles.patterns[column][1] = base->ram_[colour_base + AddressT((base->tile_offset_ << 3) >> colour_name_shift)];
|
||||
}
|
||||
|
||||
|
@ -11,16 +11,16 @@
|
||||
namespace TI::TMS {
|
||||
|
||||
// Genus determinants for the various personalityes.
|
||||
constexpr bool is_sega_vdp(Personality p) {
|
||||
constexpr bool is_sega_vdp(const Personality p) {
|
||||
return p >= Personality::SMSVDP;
|
||||
}
|
||||
|
||||
constexpr bool is_yamaha_vdp(Personality p) {
|
||||
constexpr bool is_yamaha_vdp(const Personality p) {
|
||||
return p == Personality::V9938 || p == Personality::V9958;
|
||||
}
|
||||
|
||||
// i.e. one with the original internal timings.
|
||||
constexpr bool is_classic_vdp(Personality p) {
|
||||
constexpr bool is_classic_vdp(const Personality p) {
|
||||
return
|
||||
p == Personality::TMS9918A ||
|
||||
p == Personality::SMSVDP ||
|
||||
@ -28,7 +28,7 @@ constexpr bool is_classic_vdp(Personality p) {
|
||||
p == Personality::GGVDP;
|
||||
}
|
||||
|
||||
constexpr size_t memory_size(Personality p) {
|
||||
constexpr size_t memory_size(const Personality p) {
|
||||
switch(p) {
|
||||
case TI::TMS::TMS9918A:
|
||||
case TI::TMS::SMSVDP:
|
||||
@ -41,7 +41,7 @@ constexpr size_t memory_size(Personality p) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr size_t memory_mask(Personality p) {
|
||||
constexpr size_t memory_mask(const Personality p) {
|
||||
return memory_size(p) - 1;
|
||||
}
|
||||
|
||||
|
@ -28,269 +28,269 @@ template <> struct Storage<Personality::TMS9918A> {
|
||||
};
|
||||
|
||||
struct YamahaFetcher {
|
||||
public:
|
||||
/// Describes an _observable_ memory access event. i.e. anything that it is safe
|
||||
/// (and convenient) to treat as atomic in between external slots.
|
||||
struct Event {
|
||||
/// Offset of the _beginning_ of the event. Not completely arbitrarily: this is when
|
||||
/// external data must be ready by in order to take part in those slots.
|
||||
uint16_t offset = 1368;
|
||||
enum class Type: uint8_t {
|
||||
/// A slot for reading or writing data on behalf of the CPU or the command engine.
|
||||
External,
|
||||
public:
|
||||
/// Describes an _observable_ memory access event. i.e. anything that it is safe
|
||||
/// (and convenient) to treat as atomic in between external slots.
|
||||
struct Event {
|
||||
/// Offset of the _beginning_ of the event. Not completely arbitrarily: this is when
|
||||
/// external data must be ready by in order to take part in those slots.
|
||||
uint16_t offset = 1368;
|
||||
enum class Type: uint8_t {
|
||||
/// A slot for reading or writing data on behalf of the CPU or the command engine.
|
||||
External,
|
||||
|
||||
//
|
||||
// Sprites.
|
||||
//
|
||||
SpriteY,
|
||||
SpriteLocation,
|
||||
SpritePattern,
|
||||
//
|
||||
// Sprites.
|
||||
//
|
||||
SpriteY,
|
||||
SpriteLocation,
|
||||
SpritePattern,
|
||||
|
||||
//
|
||||
// Backgrounds.
|
||||
//
|
||||
Name,
|
||||
Colour,
|
||||
Pattern,
|
||||
} type = Type::External;
|
||||
uint8_t id = 0;
|
||||
//
|
||||
// Backgrounds.
|
||||
//
|
||||
Name,
|
||||
Colour,
|
||||
Pattern,
|
||||
} type = Type::External;
|
||||
uint8_t id = 0;
|
||||
|
||||
constexpr Event(Type type, uint8_t id = 0) noexcept :
|
||||
type(type),
|
||||
id(id) {}
|
||||
constexpr Event(Type type, uint8_t id = 0) noexcept :
|
||||
type(type),
|
||||
id(id) {}
|
||||
|
||||
constexpr Event() noexcept = default;
|
||||
};
|
||||
constexpr Event() noexcept = default;
|
||||
};
|
||||
|
||||
// State that tracks fetching position within a line.
|
||||
const Event *next_event_ = nullptr;
|
||||
// State that tracks fetching position within a line.
|
||||
const Event *next_event_ = nullptr;
|
||||
|
||||
// Sprite collection state.
|
||||
bool sprites_enabled_ = true;
|
||||
// Sprite collection state.
|
||||
bool sprites_enabled_ = true;
|
||||
|
||||
protected:
|
||||
/// @return 1 + the number of times within a line that @c GeneratorT produces an event.
|
||||
template <typename GeneratorT> static constexpr size_t events_size() {
|
||||
size_t size = 0;
|
||||
for(int c = 0; c < 1368; c++) {
|
||||
const auto event_type = GeneratorT::event(c);
|
||||
size += event_type.has_value();
|
||||
}
|
||||
return size + 1;
|
||||
protected:
|
||||
/// @return 1 + the number of times within a line that @c GeneratorT produces an event.
|
||||
template <typename GeneratorT> static constexpr size_t events_size() {
|
||||
size_t size = 0;
|
||||
for(int c = 0; c < 1368; c++) {
|
||||
const auto event_type = GeneratorT::event(c);
|
||||
size += event_type.has_value();
|
||||
}
|
||||
return size + 1;
|
||||
}
|
||||
|
||||
/// @return An array of all events generated by @c GeneratorT in line order.
|
||||
template <typename GeneratorT, size_t size = events_size<GeneratorT>()>
|
||||
static constexpr std::array<Event, size> events() {
|
||||
std::array<Event, size> result{};
|
||||
size_t index = 0;
|
||||
for(int c = 0; c < 1368; c++) {
|
||||
// Specific personality doesn't matter here; both Yamahas use the same internal timing.
|
||||
const auto event = GeneratorT::event(from_internal<Personality::V9938, Clock::FromStartOfSync>(c));
|
||||
if(!event) {
|
||||
continue;
|
||||
}
|
||||
result[index] = *event;
|
||||
result[index].offset = uint16_t(c);
|
||||
++index;
|
||||
/// @return An array of all events generated by @c GeneratorT in line order.
|
||||
template <typename GeneratorT, size_t size = events_size<GeneratorT>()>
|
||||
static constexpr std::array<Event, size> events() {
|
||||
std::array<Event, size> result{};
|
||||
size_t index = 0;
|
||||
for(int c = 0; c < 1368; c++) {
|
||||
// Specific personality doesn't matter here; both Yamahas use the same internal timing.
|
||||
const auto event = GeneratorT::event(from_internal<Personality::V9938, Clock::FromStartOfSync>(c));
|
||||
if(!event) {
|
||||
continue;
|
||||
}
|
||||
result[index] = Event();
|
||||
return result;
|
||||
result[index] = *event;
|
||||
result[index].offset = uint16_t(c);
|
||||
++index;
|
||||
}
|
||||
result[index] = Event();
|
||||
return result;
|
||||
}
|
||||
|
||||
struct StandardGenerators {
|
||||
static constexpr std::optional<Event> external_every_eight(int index) {
|
||||
if(index & 7) return std::nullopt;
|
||||
struct StandardGenerators {
|
||||
static constexpr std::optional<Event> external_every_eight(int index) {
|
||||
if(index & 7) return std::nullopt;
|
||||
return Event::Type::External;
|
||||
}
|
||||
};
|
||||
|
||||
struct RefreshGenerator {
|
||||
static constexpr std::optional<Event> event(const int grauw_index) {
|
||||
// From 0 to 126: CPU/CMD slots at every cycle divisible by 8.
|
||||
if(grauw_index < 126) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 0);
|
||||
}
|
||||
|
||||
// From 164 to 1234: eight-cycle windows, the first 15 of each 16 being
|
||||
// CPU/CMD and the final being refresh.
|
||||
if(grauw_index >= 164 && grauw_index < 1234) {
|
||||
const int offset = grauw_index - 164;
|
||||
if(offset & 7) return std::nullopt;
|
||||
if(((offset >> 3) & 15) == 15) return std::nullopt;
|
||||
return Event::Type::External;
|
||||
}
|
||||
};
|
||||
|
||||
struct RefreshGenerator {
|
||||
static constexpr std::optional<Event> event(int grauw_index) {
|
||||
// From 0 to 126: CPU/CMD slots at every cycle divisible by 8.
|
||||
if(grauw_index < 126) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 0);
|
||||
}
|
||||
|
||||
// From 164 to 1234: eight-cycle windows, the first 15 of each 16 being
|
||||
// CPU/CMD and the final being refresh.
|
||||
if(grauw_index >= 164 && grauw_index < 1234) {
|
||||
const int offset = grauw_index - 164;
|
||||
if(offset & 7) return std::nullopt;
|
||||
if(((offset >> 3) & 15) == 15) return std::nullopt;
|
||||
return Event::Type::External;
|
||||
}
|
||||
|
||||
// From 1268 to 1330: CPU/CMD slots at every cycle divisible by 8.
|
||||
if(grauw_index >= 1268 && grauw_index < 1330) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 1268);
|
||||
}
|
||||
|
||||
// A CPU/CMD at 1334.
|
||||
if(grauw_index == 1334) {
|
||||
return Event::Type::External;
|
||||
}
|
||||
|
||||
// From 1344 to 1366: CPU/CMD slots every cycle divisible by 8.
|
||||
if(grauw_index >= 1344 && grauw_index < 1366) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 1344);
|
||||
}
|
||||
|
||||
// Otherwise: nothing.
|
||||
return std::nullopt;
|
||||
// From 1268 to 1330: CPU/CMD slots at every cycle divisible by 8.
|
||||
if(grauw_index >= 1268 && grauw_index < 1330) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 1268);
|
||||
}
|
||||
};
|
||||
|
||||
template <bool include_sprites> struct BitmapGenerator {
|
||||
static constexpr std::optional<Event> event(int grauw_index) {
|
||||
if(!include_sprites) {
|
||||
// Various standard zones of one-every-eight external slots.
|
||||
if(grauw_index < 124) {
|
||||
return StandardGenerators::external_every_eight(grauw_index + 2);
|
||||
}
|
||||
if(grauw_index > 1266) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 1266);
|
||||
}
|
||||
} else {
|
||||
// This records collection points for all data for selected sprites.
|
||||
// There's only four of them (each site covering two sprites),
|
||||
// so it's clearer just to be explicit.
|
||||
//
|
||||
// There's also a corresponding number of extra external slots to spell out.
|
||||
switch(grauw_index) {
|
||||
default: break;
|
||||
case 1238: return Event(Event::Type::SpriteLocation, 0);
|
||||
case 1302: return Event(Event::Type::SpriteLocation, 2);
|
||||
case 2: return Event(Event::Type::SpriteLocation, 4);
|
||||
case 66: return Event(Event::Type::SpriteLocation, 6);
|
||||
case 1270: return Event(Event::Type::SpritePattern, 0);
|
||||
case 1338: return Event(Event::Type::SpritePattern, 2);
|
||||
case 34: return Event(Event::Type::SpritePattern, 4);
|
||||
case 98: return Event(Event::Type::SpritePattern, 6);
|
||||
case 1264: case 1330: case 28: case 92:
|
||||
return Event::Type::External;
|
||||
}
|
||||
}
|
||||
|
||||
if(grauw_index >= 162 && grauw_index < 176) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 162);
|
||||
}
|
||||
|
||||
// Everywhere else the pattern is:
|
||||
//
|
||||
// external or sprite y, external, data block
|
||||
//
|
||||
// Subject to caveats:
|
||||
//
|
||||
// 1) the first data block is just a dummy fetch with no side effects,
|
||||
// so this emulator declines to record it; and
|
||||
// 2) every fourth block, the second external is actually a refresh.
|
||||
//
|
||||
if(grauw_index >= 182 && grauw_index < 1238) {
|
||||
const int offset = grauw_index - 182;
|
||||
const int block = offset / 32;
|
||||
const int sub_block = offset & 31;
|
||||
|
||||
switch(sub_block) {
|
||||
default: return std::nullopt;
|
||||
case 0:
|
||||
if(include_sprites) {
|
||||
// Don't include the sprite post-amble (i.e. a spurious read with no side effects).
|
||||
if(block < 32) {
|
||||
return Event(Event::Type::SpriteY, uint8_t(block));
|
||||
}
|
||||
} else {
|
||||
return Event::Type::External;
|
||||
}
|
||||
case 6:
|
||||
if((block & 3) != 3) {
|
||||
return Event::Type::External;
|
||||
}
|
||||
break;
|
||||
case 12:
|
||||
if(block) {
|
||||
return Event(Event::Type::Pattern, uint8_t(block - 1));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
// A CPU/CMD at 1334.
|
||||
if(grauw_index == 1334) {
|
||||
return Event::Type::External;
|
||||
}
|
||||
};
|
||||
|
||||
struct TextGenerator {
|
||||
static constexpr std::optional<Event> event(int grauw_index) {
|
||||
// Capture various one-in-eight zones.
|
||||
if(grauw_index < 72) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 2);
|
||||
}
|
||||
if(grauw_index >= 166 && grauw_index < 228) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 166);
|
||||
}
|
||||
if(grauw_index >= 1206 && grauw_index < 1332) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 1206);
|
||||
}
|
||||
if(grauw_index == 1336) {
|
||||
return Event::Type::External;
|
||||
}
|
||||
if(grauw_index >= 1346) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 1346);
|
||||
}
|
||||
|
||||
// Elsewhere...
|
||||
if(grauw_index >= 246) {
|
||||
const int offset = grauw_index - 246;
|
||||
const int block = offset / 48;
|
||||
const int sub_block = offset % 48;
|
||||
switch(sub_block) {
|
||||
default: break;
|
||||
case 0: return Event(Event::Type::Name, uint8_t(block));
|
||||
case 18: return (block & 1) ? Event::Type::External : Event(Event::Type::Colour, uint8_t(block >> 1));
|
||||
case 24: return Event(Event::Type::Pattern, uint8_t(block));
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
// From 1344 to 1366: CPU/CMD slots every cycle divisible by 8.
|
||||
if(grauw_index >= 1344 && grauw_index < 1366) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 1344);
|
||||
}
|
||||
};
|
||||
|
||||
struct CharacterGenerator {
|
||||
static constexpr std::optional<Event> event(int grauw_index) {
|
||||
// Grab sprite events.
|
||||
// Otherwise: nothing.
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
template <bool include_sprites> struct BitmapGenerator {
|
||||
static constexpr std::optional<Event> event(const int grauw_index) {
|
||||
if(!include_sprites) {
|
||||
// Various standard zones of one-every-eight external slots.
|
||||
if(grauw_index < 124) {
|
||||
return StandardGenerators::external_every_eight(grauw_index + 2);
|
||||
}
|
||||
if(grauw_index > 1266) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 1266);
|
||||
}
|
||||
} else {
|
||||
// This records collection points for all data for selected sprites.
|
||||
// There's only four of them (each site covering two sprites),
|
||||
// so it's clearer just to be explicit.
|
||||
//
|
||||
// There's also a corresponding number of extra external slots to spell out.
|
||||
switch(grauw_index) {
|
||||
default: break;
|
||||
case 1242: return Event(Event::Type::SpriteLocation, 0);
|
||||
case 1306: return Event(Event::Type::SpriteLocation, 1);
|
||||
case 6: return Event(Event::Type::SpriteLocation, 2);
|
||||
case 70: return Event(Event::Type::SpriteLocation, 3);
|
||||
case 1274: return Event(Event::Type::SpritePattern, 0);
|
||||
case 1342: return Event(Event::Type::SpritePattern, 1);
|
||||
case 38: return Event(Event::Type::SpritePattern, 2);
|
||||
case 102: return Event(Event::Type::SpritePattern, 3);
|
||||
case 1268: case 1334: case 32: case 96: return Event::Type::External;
|
||||
case 1238: return Event(Event::Type::SpriteLocation, 0);
|
||||
case 1302: return Event(Event::Type::SpriteLocation, 2);
|
||||
case 2: return Event(Event::Type::SpriteLocation, 4);
|
||||
case 66: return Event(Event::Type::SpriteLocation, 6);
|
||||
case 1270: return Event(Event::Type::SpritePattern, 0);
|
||||
case 1338: return Event(Event::Type::SpritePattern, 2);
|
||||
case 34: return Event(Event::Type::SpritePattern, 4);
|
||||
case 98: return Event(Event::Type::SpritePattern, 6);
|
||||
case 1264: case 1330: case 28: case 92:
|
||||
return Event::Type::External;
|
||||
}
|
||||
|
||||
if(grauw_index >= 166 && grauw_index < 180) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 166);
|
||||
}
|
||||
|
||||
if(grauw_index >= 182 && grauw_index < 1238) {
|
||||
const int offset = grauw_index - 182;
|
||||
const int block = offset / 32;
|
||||
const int sub_block = offset & 31;
|
||||
switch(sub_block) {
|
||||
case 0: if(block > 0) return Event(Event::Type::Name, uint8_t(block - 1));
|
||||
case 6: if((sub_block & 3) != 3) return Event::Type::External;
|
||||
case 12: if(block < 32) return Event(Event::Type::SpriteY, uint8_t(block));
|
||||
case 18: if(block > 0) return Event(Event::Type::Pattern, uint8_t(block - 1));
|
||||
case 24: if(block > 0) return Event(Event::Type::Colour, uint8_t(block - 1));
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
if(grauw_index >= 162 && grauw_index < 176) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 162);
|
||||
}
|
||||
|
||||
// Everywhere else the pattern is:
|
||||
//
|
||||
// external or sprite y, external, data block
|
||||
//
|
||||
// Subject to caveats:
|
||||
//
|
||||
// 1) the first data block is just a dummy fetch with no side effects,
|
||||
// so this emulator declines to record it; and
|
||||
// 2) every fourth block, the second external is actually a refresh.
|
||||
//
|
||||
if(grauw_index >= 182 && grauw_index < 1238) {
|
||||
const int offset = grauw_index - 182;
|
||||
const int block = offset / 32;
|
||||
const int sub_block = offset & 31;
|
||||
|
||||
switch(sub_block) {
|
||||
default: return std::nullopt;
|
||||
case 0:
|
||||
if(include_sprites) {
|
||||
// Don't include the sprite post-amble (i.e. a spurious read with no side effects).
|
||||
if(block < 32) {
|
||||
return Event(Event::Type::SpriteY, uint8_t(block));
|
||||
}
|
||||
} else {
|
||||
return Event::Type::External;
|
||||
}
|
||||
case 6:
|
||||
if((block & 3) != 3) {
|
||||
return Event::Type::External;
|
||||
}
|
||||
break;
|
||||
case 12:
|
||||
if(block) {
|
||||
return Event(Event::Type::Pattern, uint8_t(block - 1));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
struct TextGenerator {
|
||||
static constexpr std::optional<Event> event(const int grauw_index) {
|
||||
// Capture various one-in-eight zones.
|
||||
if(grauw_index < 72) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 2);
|
||||
}
|
||||
if(grauw_index >= 166 && grauw_index < 228) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 166);
|
||||
}
|
||||
if(grauw_index >= 1206 && grauw_index < 1332) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 1206);
|
||||
}
|
||||
if(grauw_index == 1336) {
|
||||
return Event::Type::External;
|
||||
}
|
||||
if(grauw_index >= 1346) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 1346);
|
||||
}
|
||||
|
||||
// Elsewhere...
|
||||
if(grauw_index >= 246) {
|
||||
const int offset = grauw_index - 246;
|
||||
const int block = offset / 48;
|
||||
const int sub_block = offset % 48;
|
||||
switch(sub_block) {
|
||||
default: break;
|
||||
case 0: return Event(Event::Type::Name, uint8_t(block));
|
||||
case 18: return (block & 1) ? Event::Type::External : Event(Event::Type::Colour, uint8_t(block >> 1));
|
||||
case 24: return Event(Event::Type::Pattern, uint8_t(block));
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
struct CharacterGenerator {
|
||||
static constexpr std::optional<Event> event(const int grauw_index) {
|
||||
// Grab sprite events.
|
||||
switch(grauw_index) {
|
||||
default: break;
|
||||
case 1242: return Event(Event::Type::SpriteLocation, 0);
|
||||
case 1306: return Event(Event::Type::SpriteLocation, 1);
|
||||
case 6: return Event(Event::Type::SpriteLocation, 2);
|
||||
case 70: return Event(Event::Type::SpriteLocation, 3);
|
||||
case 1274: return Event(Event::Type::SpritePattern, 0);
|
||||
case 1342: return Event(Event::Type::SpritePattern, 1);
|
||||
case 38: return Event(Event::Type::SpritePattern, 2);
|
||||
case 102: return Event(Event::Type::SpritePattern, 3);
|
||||
case 1268: case 1334: case 32: case 96: return Event::Type::External;
|
||||
}
|
||||
|
||||
if(grauw_index >= 166 && grauw_index < 180) {
|
||||
return StandardGenerators::external_every_eight(grauw_index - 166);
|
||||
}
|
||||
|
||||
if(grauw_index >= 182 && grauw_index < 1238) {
|
||||
const int offset = grauw_index - 182;
|
||||
const int block = offset / 32;
|
||||
const int sub_block = offset & 31;
|
||||
switch(sub_block) {
|
||||
case 0: if(block > 0) return Event(Event::Type::Name, uint8_t(block - 1));
|
||||
case 6: if((sub_block & 3) != 3) return Event::Type::External;
|
||||
case 12: if(block < 32) return Event(Event::Type::SpriteY, uint8_t(block));
|
||||
case 18: if(block > 0) return Event(Event::Type::Pattern, uint8_t(block - 1));
|
||||
case 24: if(block > 0) return Event(Event::Type::Colour, uint8_t(block - 1));
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
struct YamahaCommandState {
|
||||
@ -314,7 +314,7 @@ struct YamahaCommandState {
|
||||
int minimum_command_column_ = 0;
|
||||
uint8_t command_latch_ = 0;
|
||||
|
||||
void update_command_step(int current_column) {
|
||||
void update_command_step(const int current_column) {
|
||||
if(!command_) {
|
||||
next_command_step_ = CommandStep::None;
|
||||
return;
|
||||
@ -354,7 +354,10 @@ struct YamahaCommandState {
|
||||
};
|
||||
|
||||
// Yamaha-specific storage.
|
||||
template <Personality personality> struct Storage<personality, std::enable_if_t<is_yamaha_vdp(personality)>>: public YamahaFetcher, public YamahaCommandState {
|
||||
template <Personality personality>
|
||||
struct Storage<personality, std::enable_if_t<is_yamaha_vdp(personality)>>:
|
||||
public YamahaFetcher, public YamahaCommandState
|
||||
{
|
||||
using AddressT = uint32_t;
|
||||
|
||||
// The Yamaha's (optional in real hardware) additional 64kb of expansion RAM.
|
||||
@ -410,7 +413,7 @@ template <Personality personality> struct Storage<personality, std::enable_if_t<
|
||||
}
|
||||
|
||||
/// Resets line-ephemeral state for a new line.
|
||||
void begin_line(ScreenMode mode, bool is_refresh) {
|
||||
void begin_line(const ScreenMode mode, const bool is_refresh) {
|
||||
if(is_refresh) {
|
||||
next_event_ = refresh_events.data();
|
||||
return;
|
||||
|
@ -17,13 +17,13 @@ namespace TI::TMS {
|
||||
struct Vector {
|
||||
int v[2]{};
|
||||
|
||||
template <int offset, bool high> void set(uint8_t value) {
|
||||
template <int offset, bool high> void set(const uint8_t value) {
|
||||
constexpr uint8_t mask = high ? (offset ? 0x3 : 0x1) : 0xff;
|
||||
constexpr int shift = high ? 8 : 0;
|
||||
v[offset] = (v[offset] & ~(mask << shift)) | ((value & mask) << shift);
|
||||
}
|
||||
|
||||
template <int offset> void add(int amount) {
|
||||
template <int offset> void add(const int amount) {
|
||||
v[offset] += amount;
|
||||
|
||||
if constexpr (offset == 1) {
|
||||
@ -41,7 +41,7 @@ struct Vector {
|
||||
};
|
||||
|
||||
struct Colour {
|
||||
void set(uint8_t value) {
|
||||
void set(const uint8_t value) {
|
||||
colour = value;
|
||||
colour4bpp = uint8_t((value & 0xf) | (value << 4));
|
||||
colour2bpp = uint8_t((colour4bpp & 0x33) | ((colour4bpp & 0x33) << 2));
|
||||
@ -131,7 +131,8 @@ struct Command {
|
||||
/// Current command parameters.
|
||||
CommandContext &context;
|
||||
ModeDescription &mode_description;
|
||||
Command(CommandContext &context, ModeDescription &mode_description) : context(context), mode_description(mode_description) {}
|
||||
Command(CommandContext &context, ModeDescription &mode_description) :
|
||||
context(context), mode_description(mode_description) {}
|
||||
virtual ~Command() = default;
|
||||
|
||||
/// @returns @c true if all output from this command is done; @c false otherwise.
|
||||
@ -142,7 +143,7 @@ struct Command {
|
||||
virtual void advance() = 0;
|
||||
|
||||
protected:
|
||||
template <int axis, bool include_source> void advance_axis(int offset = 1) {
|
||||
template <int axis, bool include_source> void advance_axis(const int offset = 1) {
|
||||
context.destination.add<axis>(context.arguments & (0x4 << axis) ? -offset : offset);
|
||||
if constexpr (include_source) {
|
||||
context.source.add<axis>(context.arguments & (0x4 << axis) ? -offset : offset);
|
||||
@ -161,58 +162,58 @@ namespace Commands {
|
||||
/// * 88 cycles between every pixel plot;
|
||||
/// * plus an additional 32 cycles if a step along the minor axis is taken.
|
||||
struct Line: public Command {
|
||||
public:
|
||||
Line(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) {
|
||||
// context.destination = start position;
|
||||
// context.size.v[0] = long side dots;
|
||||
// context.size.v[1] = short side dots;
|
||||
// context.arguments => direction
|
||||
public:
|
||||
Line(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) {
|
||||
// context.destination = start position;
|
||||
// context.size.v[0] = long side dots;
|
||||
// context.size.v[1] = short side dots;
|
||||
// context.arguments => direction
|
||||
|
||||
position_ = context.size.v[1];
|
||||
numerator_ = position_ << 1;
|
||||
denominator_ = context.size.v[0] << 1;
|
||||
position_ = context.size.v[1];
|
||||
numerator_ = position_ << 1;
|
||||
denominator_ = context.size.v[0] << 1;
|
||||
|
||||
cycles = 32;
|
||||
access = AccessType::PlotPoint;
|
||||
cycles = 32;
|
||||
access = AccessType::PlotPoint;
|
||||
}
|
||||
|
||||
bool done() final {
|
||||
return !context.size.v[0];
|
||||
}
|
||||
|
||||
void advance() final {
|
||||
--context.size.v[0];
|
||||
cycles = 88;
|
||||
|
||||
// b0: 1 => long direction is y;
|
||||
// 0 => long direction is x.
|
||||
//
|
||||
// b2: 1 => x direction is left;
|
||||
// 0 => x direction is right.
|
||||
//
|
||||
// b3: 1 => y direction is up;
|
||||
// 0 => y direction is down.
|
||||
if(context.arguments & 0x1) {
|
||||
advance_axis<1, false>();
|
||||
} else {
|
||||
advance_axis<0, false>();
|
||||
}
|
||||
|
||||
bool done() final {
|
||||
return !context.size.v[0];
|
||||
}
|
||||
position_ -= numerator_;
|
||||
if(position_ < 0) {
|
||||
position_ += denominator_;
|
||||
cycles += 32;
|
||||
|
||||
void advance() final {
|
||||
--context.size.v[0];
|
||||
cycles = 88;
|
||||
|
||||
// b0: 1 => long direction is y;
|
||||
// 0 => long direction is x.
|
||||
//
|
||||
// b2: 1 => x direction is left;
|
||||
// 0 => x direction is right.
|
||||
//
|
||||
// b3: 1 => y direction is up;
|
||||
// 0 => y direction is down.
|
||||
if(context.arguments & 0x1) {
|
||||
advance_axis<1, false>();
|
||||
} else {
|
||||
advance_axis<0, false>();
|
||||
}
|
||||
|
||||
position_ -= numerator_;
|
||||
if(position_ < 0) {
|
||||
position_ += denominator_;
|
||||
cycles += 32;
|
||||
|
||||
if(context.arguments & 0x1) {
|
||||
advance_axis<0, false>();
|
||||
} else {
|
||||
advance_axis<1, false>();
|
||||
}
|
||||
} else {
|
||||
advance_axis<1, false>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
int position_, numerator_, denominator_;
|
||||
private:
|
||||
int position_, numerator_, denominator_;
|
||||
};
|
||||
|
||||
// MARK: - Single pixel manipulation.
|
||||
@ -221,88 +222,90 @@ struct Line: public Command {
|
||||
///
|
||||
/// No timings are documented, so this'll output or input as quickly as possible.
|
||||
template <bool is_read> struct Point: public Command {
|
||||
public:
|
||||
Point(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) {
|
||||
cycles = 0; // TODO.
|
||||
access = is_read ? AccessType::ReadPoint : AccessType::PlotPoint;
|
||||
}
|
||||
public:
|
||||
Point(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) {
|
||||
cycles = 0; // TODO.
|
||||
access = is_read ? AccessType::ReadPoint : AccessType::PlotPoint;
|
||||
}
|
||||
|
||||
bool done() final {
|
||||
return done_;
|
||||
}
|
||||
bool done() final {
|
||||
return done_;
|
||||
}
|
||||
|
||||
void advance() final {
|
||||
done_ = true;
|
||||
}
|
||||
void advance() final {
|
||||
done_ = true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool done_ = false;
|
||||
private:
|
||||
bool done_ = false;
|
||||
};
|
||||
|
||||
// MARK: - Rectangular base.
|
||||
|
||||
/// Useful base class for anything that does logical work in a rectangle.
|
||||
template <bool logical, bool include_source> struct Rectangle: public Command {
|
||||
public:
|
||||
Rectangle(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) {
|
||||
if constexpr (include_source) {
|
||||
start_x_[0] = context.source.v[0];
|
||||
}
|
||||
start_x_[1] = context.destination.v[0];
|
||||
width_ = context.size.v[0];
|
||||
public:
|
||||
Rectangle(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) {
|
||||
if constexpr (include_source) {
|
||||
start_x_[0] = context.source.v[0];
|
||||
}
|
||||
start_x_[1] = context.destination.v[0];
|
||||
width_ = context.size.v[0];
|
||||
|
||||
if(!width_) {
|
||||
// Width = 0 => maximal width for this mode.
|
||||
// (aside: it's still unclear to me whether commands are
|
||||
// automatically clipped to the display; I think so but
|
||||
// don't want to spend any time on it until I'm certain)
|
||||
if(!width_) {
|
||||
// Width = 0 => maximal width for this mode.
|
||||
// (aside: it's still unclear to me whether commands are
|
||||
// automatically clipped to the display; I think so but
|
||||
// don't want to spend any time on it until I'm certain)
|
||||
// context.size.v[0] = width_ = mode_description.width;
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances the current destination and, if @c include_source is @c true also the source;
|
||||
/// @returns @c true if a new row was started; @c false otherwise.
|
||||
bool advance_pixel() {
|
||||
if constexpr (logical) {
|
||||
advance_axis<0, include_source>();
|
||||
--context.size.v[0];
|
||||
|
||||
if(context.size.v[0]) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
advance_axis<0, include_source>(mode_description.pixels_per_byte);
|
||||
context.size.v[0] -= mode_description.pixels_per_byte;
|
||||
|
||||
if(context.size.v[0] & ~(mode_description.pixels_per_byte - 1)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances the current destination and, if @c include_source is @c true also the source;
|
||||
/// @returns @c true if a new row was started; @c false otherwise.
|
||||
bool advance_pixel() {
|
||||
if constexpr (logical) {
|
||||
advance_axis<0, include_source>();
|
||||
--context.size.v[0];
|
||||
|
||||
if(context.size.v[0]) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
advance_axis<0, include_source>(mode_description.pixels_per_byte);
|
||||
context.size.v[0] -= mode_description.pixels_per_byte;
|
||||
|
||||
if(context.size.v[0] & ~(mode_description.pixels_per_byte - 1)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
context.size.v[0] = width_;
|
||||
if constexpr (include_source) {
|
||||
context.source.v[0] = start_x_[0];
|
||||
}
|
||||
context.destination.v[0] = start_x_[1];
|
||||
|
||||
advance_axis<1, include_source>();
|
||||
--context.size.v[1];
|
||||
|
||||
return true;
|
||||
context.size.v[0] = width_;
|
||||
if constexpr (include_source) {
|
||||
context.source.v[0] = start_x_[0];
|
||||
}
|
||||
context.destination.v[0] = start_x_[1];
|
||||
|
||||
bool done() final {
|
||||
return !context.size.v[1] || !width_;
|
||||
}
|
||||
advance_axis<1, include_source>();
|
||||
--context.size.v[1];
|
||||
|
||||
private:
|
||||
int start_x_[2]{}, width_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool done() final {
|
||||
return !context.size.v[1] || !width_;
|
||||
}
|
||||
|
||||
private:
|
||||
int start_x_[2]{}, width_ = 0;
|
||||
};
|
||||
|
||||
// MARK: - Rectangular moves to/from CPU.
|
||||
|
||||
template <bool logical> struct MoveFromCPU: public Rectangle<logical, false> {
|
||||
MoveFromCPU(CommandContext &context, ModeDescription &mode_description) : Rectangle<logical, false>(context, mode_description) {
|
||||
MoveFromCPU(CommandContext &context, ModeDescription &mode_description) :
|
||||
Rectangle<logical, false>(context, mode_description)
|
||||
{
|
||||
Command::is_cpu_transfer = true;
|
||||
|
||||
// This command is started with the first colour ready to transfer.
|
||||
|
@ -22,7 +22,11 @@ using namespace GI::AY38910;
|
||||
// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc.
|
||||
|
||||
template <bool is_stereo>
|
||||
AY38910SampleSource<is_stereo>::AY38910SampleSource(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) {
|
||||
AY38910SampleSource<is_stereo>::AY38910SampleSource(
|
||||
Personality personality,
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue)
|
||||
: task_queue_(task_queue)
|
||||
{
|
||||
// Don't use the low bit of the envelope position if this is an AY.
|
||||
envelope_position_mask_ |= personality == Personality::AY38910;
|
||||
|
||||
@ -82,7 +86,7 @@ AY38910SampleSource<is_stereo>::AY38910SampleSource(Personality personality, Con
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_sample_volume_range(std::int16_t range) {
|
||||
void AY38910SampleSource<is_stereo>::set_sample_volume_range(const std::int16_t range) {
|
||||
// Set up volume lookup table; the function below is based on a combination of the graph
|
||||
// from the YM's datasheet, showing a clear power curve, and fitting that to observed
|
||||
// values reported elsewhere.
|
||||
@ -101,7 +105,14 @@ void AY38910SampleSource<is_stereo>::set_sample_volume_range(std::int16_t range)
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) {
|
||||
void AY38910SampleSource<is_stereo>::set_output_mixing(
|
||||
const float a_left,
|
||||
const float b_left,
|
||||
const float c_left,
|
||||
const float a_right,
|
||||
const float b_right,
|
||||
const float c_right
|
||||
) {
|
||||
a_left_ = uint8_t(a_left * 255.0f);
|
||||
b_left_ = uint8_t(b_left * 255.0f);
|
||||
c_left_ = uint8_t(c_left * 255.0f);
|
||||
@ -223,12 +234,12 @@ bool AY38910SampleSource<is_stereo>::is_zero_level() const {
|
||||
// MARK: - Register manipulation
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::select_register(uint8_t r) {
|
||||
void AY38910SampleSource<is_stereo>::select_register(const uint8_t r) {
|
||||
selected_register_ = r;
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_register_value(uint8_t value) {
|
||||
void AY38910SampleSource<is_stereo>::set_register_value(const uint8_t value) {
|
||||
// There are only 16 registers.
|
||||
if(selected_register_ > 15) return;
|
||||
|
||||
@ -298,7 +309,7 @@ void AY38910SampleSource<is_stereo>::set_register_value(uint8_t value) {
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
uint8_t AY38910SampleSource<is_stereo>::get_register_value() {
|
||||
uint8_t AY38910SampleSource<is_stereo>::get_register_value() const {
|
||||
// This table ensures that bits that aren't defined within the AY are returned as 0s
|
||||
// when read, conforming to CPC-sourced unit tests.
|
||||
const uint8_t register_masks[16] = {
|
||||
@ -313,27 +324,27 @@ uint8_t AY38910SampleSource<is_stereo>::get_register_value() {
|
||||
// MARK: - Port querying
|
||||
|
||||
template <bool is_stereo>
|
||||
uint8_t AY38910SampleSource<is_stereo>::get_port_output(bool port_b) {
|
||||
uint8_t AY38910SampleSource<is_stereo>::get_port_output(const bool port_b) const {
|
||||
return registers_[port_b ? 15 : 14];
|
||||
}
|
||||
|
||||
// MARK: - Bus handling
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_port_handler(PortHandler *handler) {
|
||||
void AY38910SampleSource<is_stereo>::set_port_handler(PortHandler *const handler) {
|
||||
port_handler_ = handler;
|
||||
set_port_output(true);
|
||||
set_port_output(false);
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_data_input(uint8_t r) {
|
||||
void AY38910SampleSource<is_stereo>::set_data_input(const uint8_t r) {
|
||||
data_input_ = r;
|
||||
update_bus();
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_port_output(bool port_b) {
|
||||
void AY38910SampleSource<is_stereo>::set_port_output(const bool port_b) {
|
||||
// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor,
|
||||
// so that when in the "input" mode, all pins will read normally high". Therefore,
|
||||
// report programmer selection of input mode as creating an output of 0xff.
|
||||
@ -344,7 +355,7 @@ void AY38910SampleSource<is_stereo>::set_port_output(bool port_b) {
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
uint8_t AY38910SampleSource<is_stereo>::get_data_output() {
|
||||
uint8_t AY38910SampleSource<is_stereo>::get_data_output() const {
|
||||
if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) {
|
||||
// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the
|
||||
// value returned to the CPU when reading it is the and of the output value and any input.
|
||||
@ -361,7 +372,7 @@ uint8_t AY38910SampleSource<is_stereo>::get_data_output() {
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_control_lines(ControlLines control_lines) {
|
||||
void AY38910SampleSource<is_stereo>::set_control_lines(const ControlLines control_lines) {
|
||||
switch(int(control_lines)) {
|
||||
default: control_state_ = Inactive; break;
|
||||
|
||||
@ -377,7 +388,7 @@ void AY38910SampleSource<is_stereo>::set_control_lines(ControlLines control_line
|
||||
}
|
||||
|
||||
template <bool is_stereo>
|
||||
void AY38910SampleSource<is_stereo>::set_reset(bool active) {
|
||||
void AY38910SampleSource<is_stereo>::set_reset(const bool active) {
|
||||
if(active == reset_) return;
|
||||
reset_ = active;
|
||||
|
||||
|
@ -24,23 +24,23 @@ namespace GI::AY38910 {
|
||||
instead use AY38910.get_port_output.
|
||||
*/
|
||||
class PortHandler {
|
||||
public:
|
||||
/*!
|
||||
Requests the current input on an AY port.
|
||||
public:
|
||||
/*!
|
||||
Requests the current input on an AY port.
|
||||
|
||||
@param port_b @c true if the input being queried is Port B. @c false if it is Port A.
|
||||
*/
|
||||
virtual uint8_t get_port_input([[maybe_unused]] bool port_b) {
|
||||
return 0xff;
|
||||
}
|
||||
@param port_b @c true if the input being queried is Port B. @c false if it is Port A.
|
||||
*/
|
||||
virtual uint8_t get_port_input([[maybe_unused]] bool port_b) {
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current output on an AY port.
|
||||
/*!
|
||||
Sets the current output on an AY port.
|
||||
|
||||
@param port_b @c true if the output being posted is Port B. @c false if it is Port A.
|
||||
@param value the value now being output.
|
||||
*/
|
||||
virtual void set_port_output([[maybe_unused]] bool port_b, [[maybe_unused]] uint8_t value) {}
|
||||
@param port_b @c true if the output being posted is Port B. @c false if it is Port A.
|
||||
@param value the value now being output.
|
||||
*/
|
||||
virtual void set_port_output([[maybe_unused]] bool port_b, [[maybe_unused]] uint8_t value) {}
|
||||
};
|
||||
|
||||
/*!
|
||||
@ -67,110 +67,110 @@ enum class Personality {
|
||||
This AY has an attached mono or stereo mixer.
|
||||
*/
|
||||
template <bool stereo> class AY38910SampleSource {
|
||||
public:
|
||||
/// Creates a new AY38910.
|
||||
AY38910SampleSource(Personality, Concurrency::AsyncTaskQueue<false> &);
|
||||
AY38910SampleSource(const AY38910SampleSource &) = delete;
|
||||
public:
|
||||
/// Creates a new AY38910.
|
||||
AY38910SampleSource(Personality, Concurrency::AsyncTaskQueue<false> &);
|
||||
AY38910SampleSource(const AY38910SampleSource &) = delete;
|
||||
|
||||
/// Sets the value the AY would read from its data lines if it were not outputting.
|
||||
void set_data_input(uint8_t r);
|
||||
/// Sets the value the AY would read from its data lines if it were not outputting.
|
||||
void set_data_input(uint8_t r);
|
||||
|
||||
/// Gets the value that would appear on the data lines if only the AY is outputting.
|
||||
uint8_t get_data_output();
|
||||
/// Gets the value that would appear on the data lines if only the AY is outputting.
|
||||
uint8_t get_data_output() const;
|
||||
|
||||
/// Sets the current control line state, as a bit field.
|
||||
void set_control_lines(ControlLines control_lines);
|
||||
/// Sets the current control line state, as a bit field.
|
||||
void set_control_lines(ControlLines control_lines);
|
||||
|
||||
/// Strobes the reset line.
|
||||
void reset();
|
||||
/// Strobes the reset line.
|
||||
void reset();
|
||||
|
||||
/// Sets the current value of the reset line.
|
||||
void set_reset(bool reset);
|
||||
/// Sets the current value of the reset line.
|
||||
void set_reset(bool reset);
|
||||
|
||||
/*!
|
||||
Gets the value that would appear on the requested interface port if it were in output mode.
|
||||
@parameter port_b @c true to get the value for Port B, @c false to get the value for Port A.
|
||||
*/
|
||||
uint8_t get_port_output(bool port_b);
|
||||
/*!
|
||||
Gets the value that would appear on the requested interface port if it were in output mode.
|
||||
@parameter port_b @c true to get the value for Port B, @c false to get the value for Port A.
|
||||
*/
|
||||
uint8_t get_port_output(bool port_b) const;
|
||||
|
||||
/*!
|
||||
Sets the port handler, which will receive a call every time the AY either wants to sample
|
||||
input or else declare new output. As a convenience, current port output can be obtained
|
||||
without installing a port handler via get_port_output.
|
||||
*/
|
||||
void set_port_handler(PortHandler *);
|
||||
/*!
|
||||
Sets the port handler, which will receive a call every time the AY either wants to sample
|
||||
input or else declare new output. As a convenience, current port output can be obtained
|
||||
without installing a port handler via get_port_output.
|
||||
*/
|
||||
void set_port_handler(PortHandler *);
|
||||
|
||||
/*!
|
||||
Enables or disables stereo output; if stereo output is enabled then also sets the weight of each of the AY's
|
||||
channels in each of the output channels.
|
||||
/*!
|
||||
Enables or disables stereo output; if stereo output is enabled then also sets the weight of each of the AY's
|
||||
channels in each of the output channels.
|
||||
|
||||
If a_left_ = b_left = c_left = a_right = b_right = c_right = 1.0 then you'll get output that's effectively mono.
|
||||
If a_left_ = b_left = c_left = a_right = b_right = c_right = 1.0 then you'll get output that's effectively mono.
|
||||
|
||||
a_left = 0.0, a_right = 1.0 will make A full volume on the right output, and silent on the left.
|
||||
a_left = 0.0, a_right = 1.0 will make A full volume on the right output, and silent on the left.
|
||||
|
||||
a_left = 0.5, a_right = 0.5 will make A half volume on both outputs.
|
||||
*/
|
||||
void set_output_mixing(float a_left, float b_left, float c_left, float a_right = 1.0, float b_right = 1.0, float c_right = 1.0);
|
||||
a_left = 0.5, a_right = 0.5 will make A half volume on both outputs.
|
||||
*/
|
||||
void set_output_mixing(float a_left, float b_left, float c_left, float a_right = 1.0, float b_right = 1.0, float c_right = 1.0);
|
||||
|
||||
// Sample generation.
|
||||
typename Outputs::Speaker::SampleT<stereo>::type level() const;
|
||||
void advance();
|
||||
bool is_zero_level() const;
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
// Sample generation.
|
||||
typename Outputs::Speaker::SampleT<stereo>::type level() const;
|
||||
void advance();
|
||||
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_;
|
||||
|
||||
bool reset_ = false;
|
||||
bool reset_ = false;
|
||||
|
||||
int selected_register_ = 0;
|
||||
uint8_t registers_[16]{};
|
||||
uint8_t output_registers_[16]{};
|
||||
int selected_register_ = 0;
|
||||
uint8_t registers_[16]{};
|
||||
uint8_t output_registers_[16]{};
|
||||
|
||||
int tone_periods_[3]{};
|
||||
int tone_counters_[3]{};
|
||||
int tone_outputs_[3]{};
|
||||
int tone_periods_[3]{};
|
||||
int tone_counters_[3]{};
|
||||
int tone_outputs_[3]{};
|
||||
|
||||
int noise_period_ = 0;
|
||||
int noise_counter_ = 0;
|
||||
int noise_shift_register_ = 0xffff;
|
||||
int noise_output_ = 0;
|
||||
int noise_period_ = 0;
|
||||
int noise_counter_ = 0;
|
||||
int noise_shift_register_ = 0xffff;
|
||||
int noise_output_ = 0;
|
||||
|
||||
int envelope_period_ = 0;
|
||||
int envelope_divider_ = 0;
|
||||
int envelope_position_ = 0, envelope_position_mask_ = 0;
|
||||
int envelope_shapes_[16][64];
|
||||
int envelope_overflow_masks_[16];
|
||||
int envelope_period_ = 0;
|
||||
int envelope_divider_ = 0;
|
||||
int envelope_position_ = 0, envelope_position_mask_ = 0;
|
||||
int envelope_shapes_[16][64];
|
||||
int envelope_overflow_masks_[16];
|
||||
|
||||
int volumes_[32];
|
||||
int volumes_[32];
|
||||
|
||||
enum ControlState {
|
||||
Inactive,
|
||||
LatchAddress,
|
||||
Read,
|
||||
Write
|
||||
} control_state_;
|
||||
enum ControlState {
|
||||
Inactive,
|
||||
LatchAddress,
|
||||
Read,
|
||||
Write
|
||||
} control_state_;
|
||||
|
||||
void select_register(uint8_t r);
|
||||
void set_register_value(uint8_t value);
|
||||
uint8_t get_register_value();
|
||||
void select_register(uint8_t r);
|
||||
void set_register_value(uint8_t value);
|
||||
uint8_t get_register_value() const;
|
||||
|
||||
uint8_t data_input_, data_output_;
|
||||
uint8_t data_input_, data_output_;
|
||||
|
||||
typename Outputs::Speaker::SampleT<stereo>::type output_volume_;
|
||||
typename Outputs::Speaker::SampleT<stereo>::type output_volume_;
|
||||
|
||||
void update_bus();
|
||||
PortHandler *port_handler_ = nullptr;
|
||||
void set_port_output(bool port_b);
|
||||
void update_bus();
|
||||
PortHandler *port_handler_ = nullptr;
|
||||
void set_port_output(bool port_b);
|
||||
|
||||
void evaluate_output_volume();
|
||||
void evaluate_output_volume();
|
||||
|
||||
// Output mixing control.
|
||||
uint8_t a_left_ = 255, a_right_ = 255;
|
||||
uint8_t b_left_ = 255, b_right_ = 255;
|
||||
uint8_t c_left_ = 255, c_right_ = 255;
|
||||
// Output mixing control.
|
||||
uint8_t a_left_ = 255, a_right_ = 255;
|
||||
uint8_t b_left_ = 255, b_right_ = 255;
|
||||
uint8_t c_left_ = 255, c_right_ = 255;
|
||||
|
||||
friend struct State;
|
||||
friend struct State;
|
||||
};
|
||||
|
||||
/// Defines a default AY to be the sample source with a master divider of 4;
|
||||
@ -193,7 +193,9 @@ template <bool stereo> struct AY38910:
|
||||
*/
|
||||
struct Utility {
|
||||
template <typename AY> static void write(AY &ay, bool is_data_write, uint8_t data) {
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | (is_data_write ? 0 : GI::AY38910::BC1)));
|
||||
ay.set_control_lines(
|
||||
GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | (is_data_write ? 0 : GI::AY38910::BC1))
|
||||
);
|
||||
ay.set_data_input(data);
|
||||
ay.set_control_lines(GI::AY38910::ControlLines(0));
|
||||
}
|
||||
|
@ -20,273 +20,272 @@ namespace Apple::Clock {
|
||||
will also signal interrupts — this is just the storage and time counting.
|
||||
*/
|
||||
class ClockStorage {
|
||||
public:
|
||||
/*!
|
||||
Advances the clock by 1 second.
|
||||
public:
|
||||
/*!
|
||||
Advances the clock by 1 second.
|
||||
|
||||
The caller should also signal an interrupt if applicable.
|
||||
The caller should also signal an interrupt if applicable.
|
||||
*/
|
||||
void update() {
|
||||
for(size_t c = 0; c < 4; ++c) {
|
||||
++seconds_[c];
|
||||
if(seconds_[c]) break;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current [P/B]RAM contents.
|
||||
*/
|
||||
template <typename CollectionT> void set_data(const CollectionT &collection) {
|
||||
set_data(collection.begin(), collection.end());
|
||||
}
|
||||
|
||||
template <typename IteratorT> void set_data(IteratorT begin, const IteratorT end) {
|
||||
size_t c = 0;
|
||||
while(begin != end && c < 256) {
|
||||
data_[c] = *begin;
|
||||
++begin;
|
||||
++c;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
static constexpr uint16_t NoResult = 0x100;
|
||||
static constexpr uint16_t DidComplete = 0x101;
|
||||
uint16_t perform(const uint8_t command) {
|
||||
/*
|
||||
Documented commands:
|
||||
|
||||
z0000001 Seconds register 0 (lowest order byte)
|
||||
z0000101 Seconds register 1
|
||||
z0001001 Seconds register 2
|
||||
z0001101 Seconds register 3
|
||||
00110001 Test register (write only)
|
||||
00110101 Write-protect register (write only)
|
||||
z010aa01 RAM addresses 0x10 - 0x13
|
||||
z1aaaa01 RAM addresses 0x00 – 0x0f
|
||||
|
||||
z0111abc, followed by 0defgh00
|
||||
RAM address abcdefgh
|
||||
|
||||
z = 1 => a read; z = 0 => a write.
|
||||
|
||||
The top bit of the write-protect register enables (0) or disables (1)
|
||||
writes to other locations.
|
||||
|
||||
All the documentation says about the test register is to set the top
|
||||
two bits to 0 for normal operation. Abnormal operation is undefined.
|
||||
*/
|
||||
void update() {
|
||||
for(size_t c = 0; c < 4; ++c) {
|
||||
++seconds_[c];
|
||||
if(seconds_[c]) break;
|
||||
}
|
||||
}
|
||||
switch(phase_) {
|
||||
case Phase::Command:
|
||||
// Decode an address.
|
||||
switch(command & 0x70) {
|
||||
default:
|
||||
if(command & 0x40) {
|
||||
// RAM addresses 0x00 – 0x0f.
|
||||
address_ = (command >> 2) & 0xf;
|
||||
} else return DidComplete; // Unrecognised.
|
||||
break;
|
||||
|
||||
/*!
|
||||
Sets the current [P/B]RAM contents.
|
||||
*/
|
||||
template <typename CollectionT> void set_data(const CollectionT &collection) {
|
||||
set_data(collection.begin(), collection.end());
|
||||
}
|
||||
|
||||
template <typename IteratorT> void set_data(IteratorT begin, const IteratorT end) {
|
||||
size_t c = 0;
|
||||
while(begin != end && c < 256) {
|
||||
data_[c] = *begin;
|
||||
++begin;
|
||||
++c;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
static constexpr uint16_t NoResult = 0x100;
|
||||
static constexpr uint16_t DidComplete = 0x101;
|
||||
uint16_t perform(uint8_t command) {
|
||||
/*
|
||||
Documented commands:
|
||||
|
||||
z0000001 Seconds register 0 (lowest order byte)
|
||||
z0000101 Seconds register 1
|
||||
z0001001 Seconds register 2
|
||||
z0001101 Seconds register 3
|
||||
00110001 Test register (write only)
|
||||
00110101 Write-protect register (write only)
|
||||
z010aa01 RAM addresses 0x10 - 0x13
|
||||
z1aaaa01 RAM addresses 0x00 – 0x0f
|
||||
|
||||
z0111abc, followed by 0defgh00
|
||||
RAM address abcdefgh
|
||||
|
||||
z = 1 => a read; z = 0 => a write.
|
||||
|
||||
The top bit of the write-protect register enables (0) or disables (1)
|
||||
writes to other locations.
|
||||
|
||||
All the documentation says about the test register is to set the top
|
||||
two bits to 0 for normal operation. Abnormal operation is undefined.
|
||||
*/
|
||||
switch(phase_) {
|
||||
case Phase::Command:
|
||||
// Decode an address.
|
||||
switch(command & 0x70) {
|
||||
default:
|
||||
if(command & 0x40) {
|
||||
// RAM addresses 0x00 – 0x0f.
|
||||
address_ = (command >> 2) & 0xf;
|
||||
} else return DidComplete; // Unrecognised.
|
||||
break;
|
||||
|
||||
case 0x00:
|
||||
// A time access.
|
||||
address_ = SecondsBuffer + ((command >> 2)&3);
|
||||
break;
|
||||
case 0x30:
|
||||
// Either a register access or an extended instruction.
|
||||
if(command & 0x08) {
|
||||
address_ = unsigned((command & 0x7) << 5);
|
||||
phase_ = (command & 0x80) ? Phase::SecondAddressByteRead : Phase::SecondAddressByteWrite;
|
||||
return NoResult;
|
||||
} else {
|
||||
address_ = (command & 4) ? RegisterWriteProtect : RegisterTest;
|
||||
}
|
||||
break;
|
||||
case 0x20:
|
||||
// RAM addresses 0x10 – 0x13.
|
||||
address_ = 0x10 + ((command >> 2) & 0x3);
|
||||
break;
|
||||
}
|
||||
|
||||
// If this is a read, return a result; otherwise prepare to write.
|
||||
if(command & 0x80) {
|
||||
// The two registers are write-only.
|
||||
if(address_ == RegisterTest || address_ == RegisterWriteProtect) {
|
||||
return DidComplete;
|
||||
}
|
||||
return (address_ >= SecondsBuffer) ? seconds_[address_ & 0xff] : data_[address_];
|
||||
}
|
||||
phase_ = Phase::WriteData;
|
||||
return NoResult;
|
||||
|
||||
case Phase::SecondAddressByteRead:
|
||||
case Phase::SecondAddressByteWrite:
|
||||
if(command & 0x83) {
|
||||
return DidComplete;
|
||||
}
|
||||
address_ |= command >> 2;
|
||||
|
||||
if(phase_ == Phase::SecondAddressByteRead) {
|
||||
phase_ = Phase::Command;
|
||||
return data_[address_]; // Only RAM accesses can get this far.
|
||||
} else {
|
||||
phase_ = Phase::WriteData;
|
||||
}
|
||||
return NoResult;
|
||||
|
||||
case Phase::WriteData:
|
||||
// First test: is this to the write-protect register?
|
||||
if(address_ == RegisterWriteProtect) {
|
||||
write_protect_ = command;
|
||||
return DidComplete;
|
||||
}
|
||||
|
||||
if(address_ == RegisterTest) {
|
||||
// No documentation here.
|
||||
return DidComplete;
|
||||
}
|
||||
|
||||
// No other writing is permitted if the write protect
|
||||
// register won't allow it.
|
||||
if(!(write_protect_ & 0x80)) {
|
||||
if(address_ >= SecondsBuffer) {
|
||||
seconds_[address_ & 0xff] = command;
|
||||
case 0x00:
|
||||
// A time access.
|
||||
address_ = SecondsBuffer + ((command >> 2)&3);
|
||||
break;
|
||||
case 0x30:
|
||||
// Either a register access or an extended instruction.
|
||||
if(command & 0x08) {
|
||||
address_ = unsigned((command & 0x7) << 5);
|
||||
phase_ = (command & 0x80) ? Phase::SecondAddressByteRead : Phase::SecondAddressByteWrite;
|
||||
return NoResult;
|
||||
} else {
|
||||
data_[address_] = command;
|
||||
address_ = (command & 4) ? RegisterWriteProtect : RegisterTest;
|
||||
}
|
||||
break;
|
||||
case 0x20:
|
||||
// RAM addresses 0x10 – 0x13.
|
||||
address_ = 0x10 + ((command >> 2) & 0x3);
|
||||
break;
|
||||
}
|
||||
|
||||
// If this is a read, return a result; otherwise prepare to write.
|
||||
if(command & 0x80) {
|
||||
// The two registers are write-only.
|
||||
if(address_ == RegisterTest || address_ == RegisterWriteProtect) {
|
||||
return DidComplete;
|
||||
}
|
||||
|
||||
phase_ = Phase::Command;
|
||||
return DidComplete;
|
||||
}
|
||||
|
||||
return (address_ >= SecondsBuffer) ? seconds_[address_ & 0xff] : data_[address_];
|
||||
}
|
||||
phase_ = Phase::WriteData;
|
||||
return NoResult;
|
||||
|
||||
case Phase::SecondAddressByteRead:
|
||||
case Phase::SecondAddressByteWrite:
|
||||
if(command & 0x83) {
|
||||
return DidComplete;
|
||||
}
|
||||
address_ |= command >> 2;
|
||||
|
||||
if(phase_ == Phase::SecondAddressByteRead) {
|
||||
phase_ = Phase::Command;
|
||||
return data_[address_]; // Only RAM accesses can get this far.
|
||||
} else {
|
||||
phase_ = Phase::WriteData;
|
||||
}
|
||||
return NoResult;
|
||||
|
||||
case Phase::WriteData:
|
||||
// First test: is this to the write-protect register?
|
||||
if(address_ == RegisterWriteProtect) {
|
||||
write_protect_ = command;
|
||||
return DidComplete;
|
||||
}
|
||||
|
||||
if(address_ == RegisterTest) {
|
||||
// No documentation here.
|
||||
return DidComplete;
|
||||
}
|
||||
|
||||
// No other writing is permitted if the write protect
|
||||
// register won't allow it.
|
||||
if(!(write_protect_ & 0x80)) {
|
||||
if(address_ >= SecondsBuffer) {
|
||||
seconds_[address_ & 0xff] = command;
|
||||
} else {
|
||||
data_[address_] = command;
|
||||
}
|
||||
}
|
||||
|
||||
phase_ = Phase::Command;
|
||||
return DidComplete;
|
||||
}
|
||||
|
||||
return NoResult;
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<uint8_t, 256> data_{0xff};
|
||||
std::array<uint8_t, 4> seconds_{};
|
||||
uint8_t write_protect_ = 0;
|
||||
unsigned int address_ = 0;
|
||||
|
||||
static constexpr int SecondsBuffer = 0x100;
|
||||
static constexpr int RegisterTest = 0x200;
|
||||
static constexpr int RegisterWriteProtect = 0x201;
|
||||
private:
|
||||
std::array<uint8_t, 256> data_{0xff};
|
||||
std::array<uint8_t, 4> seconds_{};
|
||||
uint8_t write_protect_ = 0;
|
||||
unsigned int address_ = 0;
|
||||
|
||||
enum class Phase {
|
||||
Command,
|
||||
SecondAddressByteRead,
|
||||
SecondAddressByteWrite,
|
||||
WriteData
|
||||
};
|
||||
Phase phase_ = Phase::Command;
|
||||
static constexpr int SecondsBuffer = 0x100;
|
||||
static constexpr int RegisterTest = 0x200;
|
||||
static constexpr int RegisterWriteProtect = 0x201;
|
||||
|
||||
enum class Phase {
|
||||
Command,
|
||||
SecondAddressByteRead,
|
||||
SecondAddressByteWrite,
|
||||
WriteData
|
||||
};
|
||||
Phase phase_ = Phase::Command;
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides the serial interface implemented by the Macintosh.
|
||||
*/
|
||||
class SerialClock: public ClockStorage {
|
||||
public:
|
||||
/*!
|
||||
Sets the current clock and data inputs to the clock.
|
||||
*/
|
||||
void set_input(bool clock, bool data) {
|
||||
// The data line is valid when the clock transitions to level 0.
|
||||
if(clock && !previous_clock_) {
|
||||
// Shift into the command_ register, no matter what.
|
||||
command_ = uint16_t((command_ << 1) | (data ? 1 : 0));
|
||||
result_ <<= 1;
|
||||
public:
|
||||
/*!
|
||||
Sets the current clock and data inputs to the clock.
|
||||
*/
|
||||
void set_input(const bool clock, const bool data) {
|
||||
// The data line is valid when the clock transitions to level 0.
|
||||
if(clock && !previous_clock_) {
|
||||
// Shift into the command_ register, no matter what.
|
||||
command_ = uint16_t((command_ << 1) | (data ? 1 : 0));
|
||||
result_ <<= 1;
|
||||
|
||||
// Increment phase.
|
||||
++phase_;
|
||||
// Increment phase.
|
||||
++phase_;
|
||||
|
||||
// If a whole byte has been collected, push it onwards.
|
||||
if(!(phase_&7)) {
|
||||
// Begin pessimistically.
|
||||
const auto effect = perform(uint8_t(command_));
|
||||
// If a whole byte has been collected, push it onwards.
|
||||
if(!(phase_&7)) {
|
||||
// Begin pessimistically.
|
||||
const auto effect = perform(uint8_t(command_));
|
||||
|
||||
switch(effect) {
|
||||
case ClockStorage::NoResult:
|
||||
break;
|
||||
default:
|
||||
result_ = uint8_t(effect);
|
||||
break;
|
||||
case ClockStorage::DidComplete:
|
||||
abort();
|
||||
break;
|
||||
}
|
||||
switch(effect) {
|
||||
case ClockStorage::NoResult:
|
||||
break;
|
||||
default:
|
||||
result_ = uint8_t(effect);
|
||||
break;
|
||||
case ClockStorage::DidComplete:
|
||||
abort();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
previous_clock_ = clock;
|
||||
}
|
||||
|
||||
/*!
|
||||
Reads the current data output level from the clock.
|
||||
*/
|
||||
bool get_data() {
|
||||
return !!(result_ & 0x80);
|
||||
}
|
||||
previous_clock_ = clock;
|
||||
}
|
||||
|
||||
/*!
|
||||
Announces that a serial command has been aborted.
|
||||
*/
|
||||
void abort() {
|
||||
result_ = 0;
|
||||
phase_ = 0;
|
||||
command_ = 0;
|
||||
}
|
||||
/*!
|
||||
Reads the current data output level from the clock.
|
||||
*/
|
||||
bool get_data() const {
|
||||
return !!(result_ & 0x80);
|
||||
}
|
||||
|
||||
private:
|
||||
int phase_ = 0;
|
||||
uint16_t command_;
|
||||
uint8_t result_ = 0;
|
||||
/*!
|
||||
Announces that a serial command has been aborted.
|
||||
*/
|
||||
void abort() {
|
||||
result_ = 0;
|
||||
phase_ = 0;
|
||||
command_ = 0;
|
||||
}
|
||||
|
||||
bool previous_clock_ = false;
|
||||
private:
|
||||
int phase_ = 0;
|
||||
uint16_t command_;
|
||||
uint8_t result_ = 0;
|
||||
|
||||
bool previous_clock_ = false;
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides the parallel interface implemented by the IIgs.
|
||||
*/
|
||||
class ParallelClock: public ClockStorage {
|
||||
public:
|
||||
void set_control(uint8_t control) {
|
||||
if(!(control&0x80)) return;
|
||||
public:
|
||||
void set_control(const uint8_t control) {
|
||||
if(!(control&0x80)) return;
|
||||
|
||||
if(control & 0x40) {
|
||||
// Read from the RTC.
|
||||
// A no-op for now.
|
||||
} else {
|
||||
// Write to the RTC. Which in this implementation also sets up a future read.
|
||||
const auto result = perform(data_);
|
||||
if(result < 0x100) {
|
||||
data_ = uint8_t(result);
|
||||
}
|
||||
if(control & 0x40) {
|
||||
// Read from the RTC.
|
||||
// A no-op for now.
|
||||
} else {
|
||||
// Write to the RTC. Which in this implementation also sets up a future read.
|
||||
const auto result = perform(data_);
|
||||
if(result < 0x100) {
|
||||
data_ = uint8_t(result);
|
||||
}
|
||||
|
||||
// MAGIC! The transaction took 0 seconds.
|
||||
// TODO: no magic.
|
||||
control_ = control & 0x7f;
|
||||
|
||||
// Bit 5 is also meant to be 1 or 0 to indicate the final byte.
|
||||
}
|
||||
|
||||
uint8_t get_control() {
|
||||
return control_;
|
||||
}
|
||||
// MAGIC! The transaction took 0 seconds.
|
||||
// TODO: no magic.
|
||||
control_ = control & 0x7f;
|
||||
|
||||
void set_data(uint8_t data) {
|
||||
data_ = data;
|
||||
}
|
||||
// Bit 5 is also meant to be 1 or 0 to indicate the final byte.
|
||||
}
|
||||
|
||||
uint8_t get_data() {
|
||||
return data_;
|
||||
}
|
||||
uint8_t get_control() const {
|
||||
return control_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t data_;
|
||||
uint8_t control_;
|
||||
void set_data(const uint8_t data) {
|
||||
data_ = data;
|
||||
}
|
||||
|
||||
uint8_t get_data() const {
|
||||
return data_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t data_;
|
||||
uint8_t control_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -15,12 +15,12 @@ using namespace Audio;
|
||||
Audio::Toggle::Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
void Toggle::set_sample_volume_range(std::int16_t range) {
|
||||
void Toggle::set_sample_volume_range(const std::int16_t range) {
|
||||
volume_ = range;
|
||||
level_ = level_active_ ? volume_ : 0;
|
||||
}
|
||||
|
||||
void Toggle::set_output(bool enabled) {
|
||||
void Toggle::set_output(const bool enabled) {
|
||||
if(is_enabled_ == enabled) return;
|
||||
is_enabled_ = enabled;
|
||||
audio_queue_.enqueue([this, enabled] {
|
||||
|
@ -17,29 +17,29 @@ namespace Audio {
|
||||
Provides a sample source that can programmatically be set to one of two values.
|
||||
*/
|
||||
class Toggle: public Outputs::Speaker::BufferSource<Toggle, false> {
|
||||
public:
|
||||
Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
public:
|
||||
Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
|
||||
Outputs::Speaker::fill<action>(target, target + number_of_samples, level_);
|
||||
}
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
bool is_zero_level() const {
|
||||
return !level_;
|
||||
}
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(const std::size_t number_of_samples, Outputs::Speaker::MonoSample *const target) {
|
||||
Outputs::Speaker::fill<action>(target, target + number_of_samples, level_);
|
||||
}
|
||||
void set_sample_volume_range(const std::int16_t range);
|
||||
bool is_zero_level() const {
|
||||
return !level_;
|
||||
}
|
||||
|
||||
void set_output(bool enabled);
|
||||
bool get_output() const;
|
||||
void set_output(bool enabled);
|
||||
bool get_output() const;
|
||||
|
||||
private:
|
||||
// Accessed on the calling thread.
|
||||
bool is_enabled_ = false;
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
private:
|
||||
// Accessed on the calling thread.
|
||||
bool is_enabled_ = false;
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
|
||||
// Accessed on the audio thread.
|
||||
int16_t level_ = 0, volume_ = 0;
|
||||
bool level_active_ = false;
|
||||
// Accessed on the audio thread.
|
||||
int16_t level_ = 0, volume_ = 0;
|
||||
bool level_active_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ namespace {
|
||||
const uint8_t input_flux = 0x1;
|
||||
}
|
||||
|
||||
DiskII::DiskII(int clock_rate) :
|
||||
DiskII::DiskII(const int clock_rate) :
|
||||
clock_rate_(clock_rate),
|
||||
inputs_(input_command),
|
||||
drives_{
|
||||
@ -38,7 +38,7 @@ DiskII::DiskII(int clock_rate) :
|
||||
drives_[active_drive_].set_event_delegate(this);
|
||||
}
|
||||
|
||||
void DiskII::set_control(Control control, bool on) {
|
||||
void DiskII::set_control(const Control control, const bool on) {
|
||||
int previous_stepper_mask = stepper_mask_;
|
||||
switch(control) {
|
||||
case Control::P0: stepper_mask_ = (stepper_mask_ & 0xe) | (on ? 0x1 : 0x0); break;
|
||||
@ -76,7 +76,7 @@ void DiskII::set_control(Control control, bool on) {
|
||||
}
|
||||
}
|
||||
|
||||
void DiskII::select_drive(int drive) {
|
||||
void DiskII::select_drive(const int drive) {
|
||||
if((drive&1) == active_drive_) return;
|
||||
|
||||
drives_[active_drive_].set_event_delegate(this);
|
||||
@ -152,7 +152,7 @@ void DiskII::run_for(const Cycles cycles) {
|
||||
}
|
||||
|
||||
void DiskII::decide_clocking_preference() {
|
||||
ClockingHint::Preference prior_preference = clocking_preference_;
|
||||
const ClockingHint::Preference prior_preference = clocking_preference_;
|
||||
|
||||
// If in read mode, clocking is either:
|
||||
//
|
||||
@ -183,7 +183,7 @@ void DiskII::decide_clocking_preference() {
|
||||
update_clocking_observer();
|
||||
}
|
||||
|
||||
bool DiskII::is_write_protected() {
|
||||
bool DiskII::is_write_protected() const {
|
||||
return (stepper_mask_ & 2) || drives_[active_drive_].get_is_read_only();
|
||||
}
|
||||
|
||||
|
@ -29,99 +29,99 @@ class DiskII :
|
||||
public Storage::Disk::Drive::EventDelegate,
|
||||
public ClockingHint::Source,
|
||||
public ClockingHint::Observer {
|
||||
public:
|
||||
DiskII(int clock_rate);
|
||||
public:
|
||||
DiskII(int clock_rate);
|
||||
|
||||
/// Sets the current external value of the data bus.
|
||||
void set_data_input(uint8_t input);
|
||||
/// Sets the current external value of the data bus.
|
||||
void set_data_input(uint8_t input);
|
||||
|
||||
/*!
|
||||
Submits an access to address @c address.
|
||||
/*!
|
||||
Submits an access to address @c address.
|
||||
|
||||
@returns The 8-bit value loaded to the data bus by the DiskII if any;
|
||||
@c DidNotLoad otherwise.
|
||||
*/
|
||||
int read_address(int address);
|
||||
@returns The 8-bit value loaded to the data bus by the DiskII if any;
|
||||
@c DidNotLoad otherwise.
|
||||
*/
|
||||
int read_address(int address);
|
||||
|
||||
/*!
|
||||
The value returned by @c read_address if accessing that address
|
||||
didn't cause the disk II to place anything onto the bus.
|
||||
*/
|
||||
static constexpr int DidNotLoad = -1;
|
||||
/*!
|
||||
The value returned by @c read_address if accessing that address
|
||||
didn't cause the disk II to place anything onto the bus.
|
||||
*/
|
||||
static constexpr int DidNotLoad = -1;
|
||||
|
||||
/// Advances the controller by @c cycles.
|
||||
void run_for(const Cycles cycles);
|
||||
/// Advances the controller by @c cycles.
|
||||
void run_for(const Cycles cycles);
|
||||
|
||||
/*!
|
||||
Supplies the image of the state machine (i.e. P6) ROM,
|
||||
which dictates how the Disk II will respond to input.
|
||||
/*!
|
||||
Supplies the image of the state machine (i.e. P6) ROM,
|
||||
which dictates how the Disk II will respond to input.
|
||||
|
||||
To reduce processing costs, some assumptions are made by
|
||||
the implementation as to the content of this ROM.
|
||||
Including:
|
||||
To reduce processing costs, some assumptions are made by
|
||||
the implementation as to the content of this ROM.
|
||||
Including:
|
||||
|
||||
* If Q6 is set and Q7 is reset, the controller is testing
|
||||
for write protect. If and when the shift register has
|
||||
become full with the state of the write protect value,
|
||||
no further processing is required.
|
||||
* If Q6 is set and Q7 is reset, the controller is testing
|
||||
for write protect. If and when the shift register has
|
||||
become full with the state of the write protect value,
|
||||
no further processing is required.
|
||||
|
||||
* If both Q6 and Q7 are reset, the drive motor is disabled,
|
||||
and the shift register is all zeroes, no further processing
|
||||
is required.
|
||||
*/
|
||||
void set_state_machine(const std::vector<uint8_t> &);
|
||||
* If both Q6 and Q7 are reset, the drive motor is disabled,
|
||||
and the shift register is all zeroes, no further processing
|
||||
is required.
|
||||
*/
|
||||
void set_state_machine(const std::vector<uint8_t> &);
|
||||
|
||||
/// Inserts @c disk into the drive @c drive.
|
||||
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
|
||||
/// Inserts @c disk into the drive @c drive.
|
||||
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
|
||||
|
||||
// As per Sleeper.
|
||||
ClockingHint::Preference preferred_clocking() const final;
|
||||
// As per Sleeper.
|
||||
ClockingHint::Preference preferred_clocking() const final;
|
||||
|
||||
// The Disk II functions as a potential target for @c Activity::Sources.
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
// The Disk II functions as a potential target for @c Activity::Sources.
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
|
||||
// Returns the Storage::Disk::Drive in use for drive @c index.
|
||||
// *NOT FOR HARDWARE EMULATION USAGE*.
|
||||
Storage::Disk::Drive &get_drive(int index);
|
||||
// Returns the Storage::Disk::Drive in use for drive @c index.
|
||||
// *NOT FOR HARDWARE EMULATION USAGE*.
|
||||
Storage::Disk::Drive &get_drive(int index);
|
||||
|
||||
private:
|
||||
enum class Control {
|
||||
P0, P1, P2, P3,
|
||||
Motor,
|
||||
};
|
||||
enum class Mode {
|
||||
Read, Write
|
||||
};
|
||||
void set_control(Control control, bool on);
|
||||
void set_mode(Mode mode);
|
||||
void select_drive(int drive);
|
||||
private:
|
||||
enum class Control {
|
||||
P0, P1, P2, P3,
|
||||
Motor,
|
||||
};
|
||||
enum class Mode {
|
||||
Read, Write
|
||||
};
|
||||
void set_control(Control control, bool on);
|
||||
void set_mode(Mode mode);
|
||||
void select_drive(int drive);
|
||||
|
||||
uint8_t trigger_address(int address, uint8_t value);
|
||||
void process_event(const Storage::Disk::Drive::Event &event) final;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) final;
|
||||
uint8_t trigger_address(int address, uint8_t value);
|
||||
void process_event(const Storage::Disk::Drive::Event &event) final;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) final;
|
||||
|
||||
const Cycles::IntType clock_rate_ = 0;
|
||||
const Cycles::IntType clock_rate_ = 0;
|
||||
|
||||
uint8_t state_ = 0;
|
||||
uint8_t inputs_ = 0;
|
||||
uint8_t shift_register_ = 0;
|
||||
uint8_t state_ = 0;
|
||||
uint8_t inputs_ = 0;
|
||||
uint8_t shift_register_ = 0;
|
||||
|
||||
int stepper_mask_ = 0;
|
||||
int stepper_position_ = 0;
|
||||
Cycles::IntType motor_off_time_ = -1;
|
||||
int stepper_mask_ = 0;
|
||||
int stepper_position_ = 0;
|
||||
Cycles::IntType motor_off_time_ = -1;
|
||||
|
||||
bool is_write_protected();
|
||||
std::array<uint8_t, 256> state_machine_;
|
||||
Storage::Disk::Drive drives_[2];
|
||||
bool drive_is_sleeping_[2];
|
||||
int active_drive_ = 0;
|
||||
bool motor_is_enabled_ = false;
|
||||
bool is_write_protected() const;
|
||||
std::array<uint8_t, 256> state_machine_;
|
||||
Storage::Disk::Drive drives_[2];
|
||||
bool drive_is_sleeping_[2];
|
||||
int active_drive_ = 0;
|
||||
bool motor_is_enabled_ = false;
|
||||
|
||||
void decide_clocking_preference();
|
||||
ClockingHint::Preference clocking_preference_ = ClockingHint::Preference::RealTime;
|
||||
void decide_clocking_preference();
|
||||
ClockingHint::Preference clocking_preference_ = ClockingHint::Preference::RealTime;
|
||||
|
||||
uint8_t data_input_ = 0;
|
||||
int flux_duration_ = 0;
|
||||
uint8_t data_input_ = 0;
|
||||
int flux_duration_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -10,16 +10,16 @@
|
||||
|
||||
using namespace Apple::Disk;
|
||||
|
||||
DiskIIDrive::DiskIIDrive(int input_clock_rate) :
|
||||
DiskIIDrive::DiskIIDrive(const int input_clock_rate) :
|
||||
IWMDrive(input_clock_rate, 1) {
|
||||
Drive::set_rotation_speed(300.0f);
|
||||
}
|
||||
|
||||
void DiskIIDrive::set_enabled(bool enabled) {
|
||||
void DiskIIDrive::set_enabled(const bool enabled) {
|
||||
set_motor_on(enabled);
|
||||
}
|
||||
|
||||
void DiskIIDrive::set_control_lines(int lines) {
|
||||
void DiskIIDrive::set_control_lines(const int lines) {
|
||||
// If the stepper magnet selections have changed, and any is on, see how
|
||||
// that moves the head.
|
||||
if(lines ^ stepper_mask_ && lines) {
|
||||
|
@ -13,16 +13,16 @@
|
||||
namespace Apple::Disk {
|
||||
|
||||
class DiskIIDrive: public IWMDrive {
|
||||
public:
|
||||
DiskIIDrive(int input_clock_rate);
|
||||
public:
|
||||
DiskIIDrive(int input_clock_rate);
|
||||
|
||||
private:
|
||||
void set_enabled(bool) final;
|
||||
void set_control_lines(int) final;
|
||||
bool read() final;
|
||||
private:
|
||||
void set_enabled(bool) final;
|
||||
void set_control_lines(int) final;
|
||||
bool read() final;
|
||||
|
||||
int stepper_mask_ = 0;
|
||||
int stepper_position_ = 0;
|
||||
int stepper_mask_ = 0;
|
||||
int stepper_position_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -21,7 +21,8 @@ namespace {
|
||||
constexpr int DRIVESEL = 1 << 5; /* This means drive select, like on the original Disk II. */
|
||||
constexpr int Q6 = 1 << 6;
|
||||
constexpr int Q7 = 1 << 7;
|
||||
constexpr int SEL = 1 << 8; /* This is an additional input, not available on a Disk II, with a confusingly-similar name to SELECT but a distinct purpose. */
|
||||
constexpr int SEL = 1 << 8; /* This is an additional input, not available on a Disk II, with a
|
||||
confusingly-similar name to SELECT but a distinct purpose. */
|
||||
|
||||
Log::Logger<Log::Source::IWM> logger;
|
||||
}
|
||||
@ -31,7 +32,7 @@ IWM::IWM(int clock_rate) :
|
||||
|
||||
// MARK: - Bus accessors
|
||||
|
||||
uint8_t IWM::read(int address) {
|
||||
uint8_t IWM::read(const int address) {
|
||||
access(address);
|
||||
|
||||
// Per Inside Macintosh:
|
||||
@ -103,7 +104,7 @@ uint8_t IWM::read(int address) {
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
void IWM::write(int address, uint8_t input) {
|
||||
void IWM::write(const int address, const uint8_t input) {
|
||||
access(address);
|
||||
|
||||
switch(state_ & (Q6 | Q7 | ENABLE)) {
|
||||
@ -205,7 +206,7 @@ void IWM::access(int address) {
|
||||
}
|
||||
}
|
||||
|
||||
void IWM::set_select(bool enabled) {
|
||||
void IWM::set_select(const bool enabled) {
|
||||
// Store SEL as an extra state bit.
|
||||
if(enabled) state_ |= SEL;
|
||||
else state_ &= ~SEL;
|
||||
@ -367,7 +368,7 @@ void IWM::process_event(const Storage::Disk::Drive::Event &event) {
|
||||
}
|
||||
}
|
||||
|
||||
void IWM::propose_shift(uint8_t bit) {
|
||||
void IWM::propose_shift(const uint8_t bit) {
|
||||
// TODO: synchronous mode.
|
||||
|
||||
// logger.info().append("Shifting at %d", cycles_since_shift_.as<int>());
|
||||
@ -397,7 +398,7 @@ void IWM::propose_shift(uint8_t bit) {
|
||||
cycles_since_shift_ -= bit_length_;
|
||||
}
|
||||
|
||||
void IWM::set_drive(int slot, IWMDrive *drive) {
|
||||
void IWM::set_drive(const int slot, IWMDrive *const drive) {
|
||||
drives_[slot] = drive;
|
||||
if(drive) {
|
||||
drive->set_event_delegate(this);
|
||||
@ -407,7 +408,10 @@ void IWM::set_drive(int slot, IWMDrive *drive) {
|
||||
}
|
||||
}
|
||||
|
||||
void IWM::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) {
|
||||
void IWM::set_component_prefers_clocking(
|
||||
ClockingHint::Source *const component,
|
||||
const ClockingHint::Preference clocking
|
||||
) {
|
||||
const bool is_rotating = clocking != ClockingHint::Preference::None;
|
||||
|
||||
if(drives_[0] && component == static_cast<ClockingHint::Source *>(drives_[0])) {
|
||||
@ -417,7 +421,7 @@ void IWM::set_component_prefers_clocking(ClockingHint::Source *component, Clocki
|
||||
}
|
||||
}
|
||||
|
||||
void IWM::set_activity_observer(Activity::Observer *observer) {
|
||||
void IWM::set_activity_observer(Activity::Observer *const observer) {
|
||||
if(drives_[0]) drives_[0]->set_activity_observer(observer, "Internal Floppy", true);
|
||||
if(drives_[1]) drives_[1]->set_activity_observer(observer, "External Floppy", true);
|
||||
}
|
||||
|
@ -45,77 +45,77 @@ struct IWMDrive: public Storage::Disk::Drive {
|
||||
class IWM:
|
||||
public Storage::Disk::Drive::EventDelegate,
|
||||
public ClockingHint::Observer {
|
||||
public:
|
||||
IWM(int clock_rate);
|
||||
public:
|
||||
IWM(int clock_rate);
|
||||
|
||||
/// Sets the current external value of the data bus.
|
||||
void write(int address, uint8_t value);
|
||||
/// Sets the current external value of the data bus.
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*!
|
||||
Submits an access to address @c address.
|
||||
/*!
|
||||
Submits an access to address @c address.
|
||||
|
||||
@returns The 8-bit value loaded to the data bus by the IWM.
|
||||
*/
|
||||
uint8_t read(int address);
|
||||
@returns The 8-bit value loaded to the data bus by the IWM.
|
||||
*/
|
||||
uint8_t read(int address);
|
||||
|
||||
/*!
|
||||
Sets the current input of the IWM's SEL line.
|
||||
*/
|
||||
void set_select(bool enabled);
|
||||
/*!
|
||||
Sets the current input of the IWM's SEL line.
|
||||
*/
|
||||
void set_select(bool);
|
||||
|
||||
/// Advances the controller by @c cycles.
|
||||
void run_for(const Cycles cycles);
|
||||
/// Advances the controller.
|
||||
void run_for(const Cycles);
|
||||
|
||||
/// Connects a drive to the IWM.
|
||||
void set_drive(int slot, IWMDrive *drive);
|
||||
/// Connects a drive to the IWM.
|
||||
void set_drive(int slot, IWMDrive *);
|
||||
|
||||
/// Registers the currently-connected drives as @c Activity::Sources ;
|
||||
/// the first will be declared 'Internal', the second 'External'.
|
||||
void set_activity_observer(Activity::Observer *observer);
|
||||
/// Registers the currently-connected drives as @c Activity::Sources ;
|
||||
/// the first will be declared 'Internal', the second 'External'.
|
||||
void set_activity_observer(Activity::Observer *);
|
||||
|
||||
private:
|
||||
// Storage::Disk::Drive::EventDelegate.
|
||||
void process_event(const Storage::Disk::Drive::Event &event) final;
|
||||
private:
|
||||
// Storage::Disk::Drive::EventDelegate.
|
||||
void process_event(const Storage::Disk::Drive::Event &) final;
|
||||
|
||||
const int clock_rate_;
|
||||
const int clock_rate_;
|
||||
|
||||
uint8_t data_register_ = 0;
|
||||
uint8_t mode_ = 0;
|
||||
// These related to functionality not-yet implemented.
|
||||
// bool read_write_ready_ = true;
|
||||
// bool write_overran_ = false;
|
||||
uint8_t data_register_ = 0;
|
||||
uint8_t mode_ = 0;
|
||||
// These related to functionality not-yet implemented.
|
||||
// bool read_write_ready_ = true;
|
||||
// bool write_overran_ = false;
|
||||
|
||||
int state_ = 0;
|
||||
int state_ = 0;
|
||||
|
||||
int active_drive_ = 0;
|
||||
IWMDrive *drives_[2] = {nullptr, nullptr};
|
||||
bool drive_is_rotating_[2] = {false, false};
|
||||
int active_drive_ = 0;
|
||||
IWMDrive *drives_[2] = {nullptr, nullptr};
|
||||
bool drive_is_rotating_[2] = {false, false};
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) final;
|
||||
|
||||
Cycles cycles_until_disable_;
|
||||
uint8_t write_handshake_ = 0x80;
|
||||
Cycles cycles_until_disable_;
|
||||
uint8_t write_handshake_ = 0x80;
|
||||
|
||||
void access(int address);
|
||||
void access(int address);
|
||||
|
||||
uint8_t shift_register_ = 0;
|
||||
uint8_t next_output_ = 0;
|
||||
int output_bits_remaining_ = 0;
|
||||
uint8_t shift_register_ = 0;
|
||||
uint8_t next_output_ = 0;
|
||||
int output_bits_remaining_ = 0;
|
||||
|
||||
void propose_shift(uint8_t bit);
|
||||
Cycles cycles_since_shift_;
|
||||
Cycles bit_length_ = Cycles(16);
|
||||
void propose_shift(uint8_t bit);
|
||||
Cycles cycles_since_shift_;
|
||||
Cycles bit_length_ = Cycles(16);
|
||||
|
||||
void push_drive_state();
|
||||
void push_drive_state();
|
||||
|
||||
enum class ShiftMode {
|
||||
Reading,
|
||||
Writing,
|
||||
CheckingWriteProtect
|
||||
} shift_mode_;
|
||||
enum class ShiftMode {
|
||||
Reading,
|
||||
Writing,
|
||||
CheckingWriteProtect
|
||||
} shift_mode_;
|
||||
|
||||
uint8_t sense();
|
||||
void select_shift_mode();
|
||||
uint8_t sense();
|
||||
void select_shift_mode();
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
DoubleDensityDrive::DoubleDensityDrive(int input_clock_rate, bool is_800k) :
|
||||
DoubleDensityDrive::DoubleDensityDrive(const int input_clock_rate, const bool is_800k) :
|
||||
IWMDrive(input_clock_rate, is_800k ? 2 : 1), // Only 800kb drives are double sided.
|
||||
is_800k_(is_800k) {
|
||||
// Start with a valid rotation speed.
|
||||
@ -29,7 +29,7 @@ DoubleDensityDrive::DoubleDensityDrive(int input_clock_rate, bool is_800k) :
|
||||
|
||||
// MARK: - Speed Selection
|
||||
|
||||
void DoubleDensityDrive::did_step(Storage::Disk::HeadPosition to_position) {
|
||||
void DoubleDensityDrive::did_step(const Storage::Disk::HeadPosition to_position) {
|
||||
// The 800kb drive automatically selects rotation speed as a function of
|
||||
// head position; the 400kb drive doesn't do so.
|
||||
if(is_800k_) {
|
||||
@ -60,7 +60,7 @@ void DoubleDensityDrive::did_step(Storage::Disk::HeadPosition to_position) {
|
||||
}
|
||||
}
|
||||
|
||||
void DoubleDensityDrive::set_rotation_speed(float revolutions_per_minute) {
|
||||
void DoubleDensityDrive::set_rotation_speed(const float revolutions_per_minute) {
|
||||
if(!is_800k_) {
|
||||
// Don't allow drive speeds to drop below 10 RPM, as a temporary sop
|
||||
// to sanity.
|
||||
@ -70,12 +70,12 @@ void DoubleDensityDrive::set_rotation_speed(float revolutions_per_minute) {
|
||||
|
||||
// MARK: - Control input/output.
|
||||
|
||||
void DoubleDensityDrive::set_enabled(bool enabled) {
|
||||
void DoubleDensityDrive::set_enabled(const bool enabled) {
|
||||
// Disabling a drive also stops its motor.
|
||||
if(!enabled) set_motor_on(false);
|
||||
}
|
||||
|
||||
void DoubleDensityDrive::set_control_lines(int lines) {
|
||||
void DoubleDensityDrive::set_control_lines(const int lines) {
|
||||
const auto old_state = control_state_;
|
||||
control_state_ = lines;
|
||||
|
||||
|
@ -13,36 +13,36 @@
|
||||
namespace Apple::Macintosh {
|
||||
|
||||
class DoubleDensityDrive: public IWMDrive {
|
||||
public:
|
||||
DoubleDensityDrive(int input_clock_rate, bool is_800k);
|
||||
public:
|
||||
DoubleDensityDrive(int input_clock_rate, bool is_800k);
|
||||
|
||||
/*!
|
||||
@returns @c true if this is an 800kb drive; @c false otherwise.
|
||||
*/
|
||||
bool is_800k() const {
|
||||
return is_800k_;
|
||||
}
|
||||
/*!
|
||||
@returns @c true if this is an 800kb drive; @c false otherwise.
|
||||
*/
|
||||
bool is_800k() const {
|
||||
return is_800k_;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current rotation speed of this drive only if it is a 400kb drive.
|
||||
800kb drives select their own rotation speed based on head position,
|
||||
and ignore this input.
|
||||
*/
|
||||
void set_rotation_speed(float revolutions_per_minute);
|
||||
/*!
|
||||
Sets the current rotation speed of this drive only if it is a 400kb drive.
|
||||
800kb drives select their own rotation speed based on head position,
|
||||
and ignore this input.
|
||||
*/
|
||||
void set_rotation_speed(float revolutions_per_minute);
|
||||
|
||||
private:
|
||||
void set_enabled(bool) final;
|
||||
void set_control_lines(int) final;
|
||||
bool read() final;
|
||||
private:
|
||||
void set_enabled(bool) final;
|
||||
void set_control_lines(int) final;
|
||||
bool read() final;
|
||||
|
||||
// To receive the proper notifications from Storage::Disk::Drive.
|
||||
void did_step(Storage::Disk::HeadPosition to_position) final;
|
||||
void did_set_disk(bool) final;
|
||||
// To receive the proper notifications from Storage::Disk::Drive.
|
||||
void did_step(Storage::Disk::HeadPosition to_position) final;
|
||||
void did_set_disk(bool) final;
|
||||
|
||||
const bool is_800k_;
|
||||
bool has_new_disk_ = false;
|
||||
int control_state_ = 0;
|
||||
int step_direction_ = 1;
|
||||
const bool is_800k_;
|
||||
bool has_new_disk_ = false;
|
||||
int control_state_ = 0;
|
||||
int step_direction_ = 1;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -18,10 +18,10 @@ Log::Logger<Log::Source::I2C> logger;
|
||||
|
||||
}
|
||||
|
||||
void Bus::set_data(bool pulled) {
|
||||
void Bus::set_data(const bool pulled) {
|
||||
set_clock_data(clock_, pulled);
|
||||
}
|
||||
bool Bus::data() {
|
||||
bool Bus::data() const {
|
||||
bool result = data_;
|
||||
if(peripheral_bits_) {
|
||||
result |= !(peripheral_response_ & 0x80);
|
||||
@ -29,14 +29,14 @@ bool Bus::data() {
|
||||
return result;
|
||||
}
|
||||
|
||||
void Bus::set_clock(bool pulled) {
|
||||
void Bus::set_clock(const bool pulled) {
|
||||
set_clock_data(pulled, data_);
|
||||
}
|
||||
bool Bus::clock() {
|
||||
bool Bus::clock() const {
|
||||
return clock_;
|
||||
}
|
||||
|
||||
void Bus::set_clock_data(bool clock_pulled, bool data_pulled) {
|
||||
void Bus::set_clock_data(const bool clock_pulled, const bool data_pulled) {
|
||||
// Proceed only if changes are evidenced.
|
||||
if(clock_pulled == clock_ && data_pulled == data_) {
|
||||
return;
|
||||
@ -95,7 +95,7 @@ void Bus::set_clock_data(bool clock_pulled, bool data_pulled) {
|
||||
}
|
||||
}
|
||||
|
||||
void Bus::signal(Event event) {
|
||||
void Bus::signal(const Event event) {
|
||||
const auto capture_bit = [&]() {
|
||||
input_ = uint16_t((input_ << 1) | (event == Event::Zero ? 0 : 1));
|
||||
++input_count_;
|
||||
@ -221,6 +221,6 @@ void Bus::signal(Event event) {
|
||||
}
|
||||
}
|
||||
|
||||
void Bus::add_peripheral(Peripheral *peripheral, int address) {
|
||||
void Bus::add_peripheral(Peripheral *const peripheral, const int address) {
|
||||
peripherals_[address] = peripheral;
|
||||
}
|
||||
|
@ -41,10 +41,10 @@ struct Peripheral {
|
||||
class Bus {
|
||||
public:
|
||||
void set_data(bool pulled);
|
||||
bool data();
|
||||
bool data() const;
|
||||
|
||||
void set_clock(bool pulled);
|
||||
bool clock();
|
||||
bool clock() const;
|
||||
|
||||
void set_clock_data(bool clock_pulled, bool data_pulled);
|
||||
|
||||
|
@ -20,7 +20,7 @@ bool SCC::is_zero_level() const {
|
||||
}
|
||||
|
||||
template <Outputs::Speaker::Action action>
|
||||
void SCC::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
|
||||
void SCC::apply_samples(const std::size_t number_of_samples, Outputs::Speaker::MonoSample *const target) {
|
||||
if(is_zero_level()) {
|
||||
Outputs::Speaker::fill<action>(target, target + number_of_samples, Outputs::Speaker::MonoSample());
|
||||
return;
|
||||
@ -55,7 +55,7 @@ template void SCC::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Out
|
||||
template void SCC::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
template void SCC::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
|
||||
|
||||
void SCC::write(uint16_t address, uint8_t value) {
|
||||
void SCC::write(uint16_t address, const uint8_t value) {
|
||||
address &= 0xff;
|
||||
if(address < 0x80) ram_[address] = value;
|
||||
|
||||
@ -108,7 +108,7 @@ void SCC::set_sample_volume_range(std::int16_t range) {
|
||||
evaluate_output_volume();
|
||||
}
|
||||
|
||||
uint8_t SCC::read(uint16_t address) {
|
||||
uint8_t SCC::read(uint16_t address) const {
|
||||
address &= 0xff;
|
||||
if(address < 0x80) {
|
||||
return ram_[address];
|
||||
|
@ -21,51 +21,51 @@ namespace Konami {
|
||||
four and five, the SCC+ supports different waves for the two channels.
|
||||
*/
|
||||
class SCC: public ::Outputs::Speaker::BufferSource<SCC, false> {
|
||||
public:
|
||||
/// Creates a new SCC.
|
||||
SCC(Concurrency::AsyncTaskQueue<false> &task_queue);
|
||||
public:
|
||||
/// Creates a new SCC.
|
||||
SCC(Concurrency::AsyncTaskQueue<false> &);
|
||||
|
||||
/// As per ::SampleSource; provides a broadphase test for silence.
|
||||
bool is_zero_level() const;
|
||||
/// As per ::SampleSource; provides a broadphase test for silence.
|
||||
bool is_zero_level() const;
|
||||
|
||||
/// As per ::SampleSource; provides audio output.
|
||||
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);
|
||||
/// As per ::SampleSource; provides audio output.
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *);
|
||||
void set_sample_volume_range(std::int16_t);
|
||||
|
||||
/// Writes to the SCC.
|
||||
void write(uint16_t address, uint8_t value);
|
||||
/// Writes to the SCC.
|
||||
void write(uint16_t address, uint8_t value);
|
||||
|
||||
/// Reads from the SCC.
|
||||
uint8_t read(uint16_t address);
|
||||
/// Reads from the SCC.
|
||||
uint8_t read(uint16_t address) const;
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
|
||||
// State from here on down is accessed ony from the audio thread.
|
||||
int master_divider_ = 0;
|
||||
std::int16_t master_volume_ = 0;
|
||||
Outputs::Speaker::MonoSample transient_output_level_ = 0;
|
||||
// State from here on down is accessed ony from the audio thread.
|
||||
int master_divider_ = 0;
|
||||
std::int16_t master_volume_ = 0;
|
||||
Outputs::Speaker::MonoSample transient_output_level_ = 0;
|
||||
|
||||
struct Channel {
|
||||
int period = 0;
|
||||
int amplitude = 0;
|
||||
struct Channel {
|
||||
int period = 0;
|
||||
int amplitude = 0;
|
||||
|
||||
int tone_counter = 0;
|
||||
int offset = 0;
|
||||
} channels_[5];
|
||||
int tone_counter = 0;
|
||||
int offset = 0;
|
||||
} channels_[5];
|
||||
|
||||
struct Wavetable {
|
||||
std::uint8_t samples[32];
|
||||
} waves_[4];
|
||||
struct Wavetable {
|
||||
std::uint8_t samples[32];
|
||||
} waves_[4];
|
||||
|
||||
std::uint8_t channel_enable_ = 0;
|
||||
std::uint8_t channel_enable_ = 0;
|
||||
|
||||
void evaluate_output_volume();
|
||||
void evaluate_output_volume();
|
||||
|
||||
// This keeps a copy of wave memory that is accessed from the
|
||||
// main emulation thread.
|
||||
std::uint8_t ram_[128];
|
||||
// This keeps a copy of wave memory that is accessed from the
|
||||
// main emulation thread.
|
||||
std::uint8_t ram_[128];
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -31,229 +31,229 @@ namespace Yamaha::OPL {
|
||||
TODO: use envelope_precision.
|
||||
*/
|
||||
template <int envelope_precision, int period_precision> class EnvelopeGenerator {
|
||||
public:
|
||||
/*!
|
||||
Advances the envelope generator a single step, given the current state of the low-frequency oscillator, @c oscillator.
|
||||
*/
|
||||
void update(const LowFrequencyOscillator &oscillator) {
|
||||
// Apply tremolo, which is fairly easy.
|
||||
tremolo_ = tremolo_enable_ * oscillator.tremolo << 4;
|
||||
public:
|
||||
/*!
|
||||
Advances the envelope generator a single step, given the current state of the low-frequency oscillator, @c oscillator.
|
||||
*/
|
||||
void update(const LowFrequencyOscillator &oscillator) {
|
||||
// Apply tremolo, which is fairly easy.
|
||||
tremolo_ = tremolo_enable_ * oscillator.tremolo << 4;
|
||||
|
||||
// Something something something...
|
||||
const int key_scaling_rate = key_scale_rate_ >> key_scale_rate_shift_;
|
||||
switch(phase_) {
|
||||
case Phase::Damp:
|
||||
update_decay(oscillator, 12 << 2);
|
||||
if(attenuation_ == 511) {
|
||||
(*will_attack_)();
|
||||
phase_ = Phase::Attack;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Attack:
|
||||
update_attack(oscillator, attack_rate_ + key_scaling_rate);
|
||||
|
||||
// Two possible terminating conditions: (i) the attack rate is 15; (ii) full volume has been reached.
|
||||
if(attenuation_ <= 0) {
|
||||
attenuation_ = 0;
|
||||
phase_ = Phase::Decay;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Decay:
|
||||
update_decay(oscillator, decay_rate_ + key_scaling_rate);
|
||||
if(attenuation_ >= sustain_level_) {
|
||||
attenuation_ = sustain_level_;
|
||||
phase_ = use_sustain_level_ ? Phase::Sustain : Phase::Release;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Sustain:
|
||||
// Nothing to do.
|
||||
break;
|
||||
|
||||
case Phase::Release:
|
||||
update_decay(oscillator, release_rate_ + key_scaling_rate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The current attenuation from this envelope generator. This is independent of the envelope precision.
|
||||
*/
|
||||
int attenuation() const {
|
||||
// TODO: if this envelope is fully released, should tremolo still be able to vocalise it?
|
||||
return (attenuation_ << 3) + tremolo_;
|
||||
}
|
||||
|
||||
/*!
|
||||
Enables or disables damping on this envelope generator. If damping is enabled then this envelope generator will
|
||||
use the damping phase when necessary (i.e. when transitioning to key on if attenuation is not already at maximum)
|
||||
and in any case will call @c will_attack before transitioning from any other state to attack.
|
||||
|
||||
@param will_attack Supply a will_attack callback to enable damping mode; supply nullopt to disable damping mode.
|
||||
*/
|
||||
void set_should_damp(const std::optional<std::function<void(void)>> &will_attack) {
|
||||
will_attack_ = will_attack;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current state of the key-on input.
|
||||
*/
|
||||
void set_key_on(bool key_on) {
|
||||
// Do nothing if this is not a leading or trailing edge.
|
||||
if(key_on == key_on_) return;
|
||||
key_on_ = key_on;
|
||||
|
||||
// Always transition to release upon a key off.
|
||||
if(!key_on_) {
|
||||
phase_ = Phase::Release;
|
||||
return;
|
||||
}
|
||||
|
||||
// On key on: if this is an envelope generator with damping, and damping is required,
|
||||
// schedule that. If damping is not required, announce a pending attack now and
|
||||
// transition to attack.
|
||||
if(will_attack_) {
|
||||
if(attenuation_ != 511) {
|
||||
phase_ = Phase::Damp;
|
||||
return;
|
||||
// Something something something...
|
||||
const int key_scaling_rate = key_scale_rate_ >> key_scale_rate_shift_;
|
||||
switch(phase_) {
|
||||
case Phase::Damp:
|
||||
update_decay(oscillator, 12 << 2);
|
||||
if(attenuation_ == 511) {
|
||||
(*will_attack_)();
|
||||
phase_ = Phase::Attack;
|
||||
}
|
||||
break;
|
||||
|
||||
(*will_attack_)();
|
||||
}
|
||||
phase_ = Phase::Attack;
|
||||
case Phase::Attack:
|
||||
update_attack(oscillator, attack_rate_ + key_scaling_rate);
|
||||
|
||||
// Two possible terminating conditions: (i) the attack rate is 15; (ii) full volume has been reached.
|
||||
if(attenuation_ <= 0) {
|
||||
attenuation_ = 0;
|
||||
phase_ = Phase::Decay;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Decay:
|
||||
update_decay(oscillator, decay_rate_ + key_scaling_rate);
|
||||
if(attenuation_ >= sustain_level_) {
|
||||
attenuation_ = sustain_level_;
|
||||
phase_ = use_sustain_level_ ? Phase::Sustain : Phase::Release;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Sustain:
|
||||
// Nothing to do.
|
||||
break;
|
||||
|
||||
case Phase::Release:
|
||||
update_decay(oscillator, release_rate_ + key_scaling_rate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The current attenuation from this envelope generator. This is independent of the envelope precision.
|
||||
*/
|
||||
int attenuation() const {
|
||||
// TODO: if this envelope is fully released, should tremolo still be able to vocalise it?
|
||||
return (attenuation_ << 3) + tremolo_;
|
||||
}
|
||||
|
||||
/*!
|
||||
Enables or disables damping on this envelope generator. If damping is enabled then this envelope generator will
|
||||
use the damping phase when necessary (i.e. when transitioning to key on if attenuation is not already at maximum)
|
||||
and in any case will call @c will_attack before transitioning from any other state to attack.
|
||||
|
||||
@param will_attack Supply a will_attack callback to enable damping mode; supply nullopt to disable damping mode.
|
||||
*/
|
||||
void set_should_damp(const std::optional<std::function<void(void)>> &will_attack) {
|
||||
will_attack_ = will_attack;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current state of the key-on input.
|
||||
*/
|
||||
void set_key_on(const bool key_on) {
|
||||
// Do nothing if this is not a leading or trailing edge.
|
||||
if(key_on == key_on_) return;
|
||||
key_on_ = key_on;
|
||||
|
||||
// Always transition to release upon a key off.
|
||||
if(!key_on_) {
|
||||
phase_ = Phase::Release;
|
||||
return;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the attack rate, which should be in the range 0–15.
|
||||
*/
|
||||
void set_attack_rate(int rate) {
|
||||
attack_rate_ = rate << 2;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the decay rate, which should be in the range 0–15.
|
||||
*/
|
||||
void set_decay_rate(int rate) {
|
||||
decay_rate_ = rate << 2;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the release rate, which should be in the range 0–15.
|
||||
*/
|
||||
void set_release_rate(int rate) {
|
||||
release_rate_ = rate << 2;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the sustain level, which should be in the range 0–15.
|
||||
*/
|
||||
void set_sustain_level(int level) {
|
||||
sustain_level_ = level << 3;
|
||||
// TODO: verify the shift level here. Especially re: precision.
|
||||
}
|
||||
|
||||
/*!
|
||||
Enables or disables use of the sustain level. If this is disabled, the envelope proceeds
|
||||
directly from decay to release.
|
||||
*/
|
||||
void set_use_sustain_level(bool use) {
|
||||
use_sustain_level_ = use;
|
||||
}
|
||||
|
||||
/*!
|
||||
Enables or disables key-rate scaling.
|
||||
*/
|
||||
void set_key_scaling_rate_enabled(bool enabled) {
|
||||
key_scale_rate_shift_ = int(enabled) * 2;
|
||||
}
|
||||
|
||||
/*!
|
||||
Enables or disables application of the low-frequency oscillator's tremolo.
|
||||
*/
|
||||
void set_tremolo_enabled(bool enabled) {
|
||||
tremolo_enable_ = int(enabled);
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current period associated with the channel that owns this envelope generator;
|
||||
this is used to select a key scaling rate if key-rate scaling is enabled.
|
||||
*/
|
||||
void set_period(int period, int octave) {
|
||||
key_scale_rate_ = (octave << 1) | (period >> (period_precision - 1));
|
||||
}
|
||||
|
||||
private:
|
||||
enum class Phase {
|
||||
Attack, Decay, Sustain, Release, Damp
|
||||
} phase_ = Phase::Release;
|
||||
int attenuation_ = 511, tremolo_ = 0;
|
||||
|
||||
bool key_on_ = false;
|
||||
std::optional<std::function<void(void)>> will_attack_;
|
||||
|
||||
int key_scale_rate_ = 0;
|
||||
int key_scale_rate_shift_ = 0;
|
||||
|
||||
int tremolo_enable_ = 0;
|
||||
|
||||
int attack_rate_ = 0;
|
||||
int decay_rate_ = 0;
|
||||
int release_rate_ = 0;
|
||||
int sustain_level_ = 0;
|
||||
bool use_sustain_level_ = false;
|
||||
|
||||
static constexpr int dithering_patterns[4][8] = {
|
||||
{0, 1, 0, 1, 0, 1, 0, 1},
|
||||
{0, 1, 0, 1, 1, 1, 0, 1},
|
||||
{0, 1, 1, 1, 0, 1, 1, 1},
|
||||
{0, 1, 1, 1, 1, 1, 1, 1},
|
||||
};
|
||||
|
||||
void update_attack(const LowFrequencyOscillator &oscillator, int rate) {
|
||||
// Special case: no attack.
|
||||
if(rate < 4) {
|
||||
// On key on: if this is an envelope generator with damping, and damping is required,
|
||||
// schedule that. If damping is not required, announce a pending attack now and
|
||||
// transition to attack.
|
||||
if(will_attack_) {
|
||||
if(attenuation_ != 511) {
|
||||
phase_ = Phase::Damp;
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: instant attack.
|
||||
if(rate >= 60) {
|
||||
attenuation_ = 0;
|
||||
return;
|
||||
}
|
||||
(*will_attack_)();
|
||||
}
|
||||
phase_ = Phase::Attack;
|
||||
}
|
||||
|
||||
// Work out the number of cycles between each adjustment tick, and stop now
|
||||
// if not at the next adjustment boundary.
|
||||
const int shift_size = 13 - (std::min(rate, 52) >> 2);
|
||||
if(oscillator.counter & ((1 << shift_size) - 1)) {
|
||||
return;
|
||||
}
|
||||
/*!
|
||||
Sets the attack rate, which should be in the range 0–15.
|
||||
*/
|
||||
void set_attack_rate(const int rate) {
|
||||
attack_rate_ = rate << 2;
|
||||
}
|
||||
|
||||
// Apply dithered adjustment.
|
||||
const int rate_shift = (rate > 55);
|
||||
const int step = dithering_patterns[rate & 3][(oscillator.counter >> shift_size) & 7];
|
||||
attenuation_ -= ((attenuation_ >> (3 - rate_shift)) + 1) * step;
|
||||
/*!
|
||||
Sets the decay rate, which should be in the range 0–15.
|
||||
*/
|
||||
void set_decay_rate(const int rate) {
|
||||
decay_rate_ = rate << 2;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the release rate, which should be in the range 0–15.
|
||||
*/
|
||||
void set_release_rate(const int rate) {
|
||||
release_rate_ = rate << 2;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the sustain level, which should be in the range 0–15.
|
||||
*/
|
||||
void set_sustain_level(const int level) {
|
||||
sustain_level_ = level << 3;
|
||||
// TODO: verify the shift level here. Especially re: precision.
|
||||
}
|
||||
|
||||
/*!
|
||||
Enables or disables use of the sustain level. If this is disabled, the envelope proceeds
|
||||
directly from decay to release.
|
||||
*/
|
||||
void set_use_sustain_level(const bool use) {
|
||||
use_sustain_level_ = use;
|
||||
}
|
||||
|
||||
/*!
|
||||
Enables or disables key-rate scaling.
|
||||
*/
|
||||
void set_key_scaling_rate_enabled(const bool enabled) {
|
||||
key_scale_rate_shift_ = int(enabled) * 2;
|
||||
}
|
||||
|
||||
/*!
|
||||
Enables or disables application of the low-frequency oscillator's tremolo.
|
||||
*/
|
||||
void set_tremolo_enabled(const bool enabled) {
|
||||
tremolo_enable_ = int(enabled);
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the current period associated with the channel that owns this envelope generator;
|
||||
this is used to select a key scaling rate if key-rate scaling is enabled.
|
||||
*/
|
||||
void set_period(const int period, const int octave) {
|
||||
key_scale_rate_ = (octave << 1) | (period >> (period_precision - 1));
|
||||
}
|
||||
|
||||
private:
|
||||
enum class Phase {
|
||||
Attack, Decay, Sustain, Release, Damp
|
||||
} phase_ = Phase::Release;
|
||||
int attenuation_ = 511, tremolo_ = 0;
|
||||
|
||||
bool key_on_ = false;
|
||||
std::optional<std::function<void(void)>> will_attack_;
|
||||
|
||||
int key_scale_rate_ = 0;
|
||||
int key_scale_rate_shift_ = 0;
|
||||
|
||||
int tremolo_enable_ = 0;
|
||||
|
||||
int attack_rate_ = 0;
|
||||
int decay_rate_ = 0;
|
||||
int release_rate_ = 0;
|
||||
int sustain_level_ = 0;
|
||||
bool use_sustain_level_ = false;
|
||||
|
||||
static constexpr int dithering_patterns[4][8] = {
|
||||
{0, 1, 0, 1, 0, 1, 0, 1},
|
||||
{0, 1, 0, 1, 1, 1, 0, 1},
|
||||
{0, 1, 1, 1, 0, 1, 1, 1},
|
||||
{0, 1, 1, 1, 1, 1, 1, 1},
|
||||
};
|
||||
|
||||
void update_attack(const LowFrequencyOscillator &oscillator, const int rate) {
|
||||
// Special case: no attack.
|
||||
if(rate < 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
void update_decay(const LowFrequencyOscillator &oscillator, int rate) {
|
||||
// Special case: no decay.
|
||||
if(rate < 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Work out the number of cycles between each adjustment tick, and stop now
|
||||
// if not at the next adjustment boundary.
|
||||
const int shift_size = 13 - (std::min(rate, 52) >> 2);
|
||||
if(oscillator.counter & ((1 << shift_size) - 1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply dithered adjustment and clamp.
|
||||
const int rate_shift = 1 + (rate > 59) + (rate > 55);
|
||||
attenuation_ += dithering_patterns[rate & 3][(oscillator.counter >> shift_size) & 7] * (4 << rate_shift);
|
||||
attenuation_ = std::min(attenuation_, 511);
|
||||
// Special case: instant attack.
|
||||
if(rate >= 60) {
|
||||
attenuation_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Work out the number of cycles between each adjustment tick, and stop now
|
||||
// if not at the next adjustment boundary.
|
||||
const int shift_size = 13 - (std::min(rate, 52) >> 2);
|
||||
if(oscillator.counter & ((1 << shift_size) - 1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply dithered adjustment.
|
||||
const int rate_shift = (rate > 55);
|
||||
const int step = dithering_patterns[rate & 3][(oscillator.counter >> shift_size) & 7];
|
||||
attenuation_ -= ((attenuation_ >> (3 - rate_shift)) + 1) * step;
|
||||
}
|
||||
|
||||
void update_decay(const LowFrequencyOscillator &oscillator, const int rate) {
|
||||
// Special case: no decay.
|
||||
if(rate < 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Work out the number of cycles between each adjustment tick, and stop now
|
||||
// if not at the next adjustment boundary.
|
||||
const int shift_size = 13 - (std::min(rate, 52) >> 2);
|
||||
if(oscillator.counter & ((1 << shift_size) - 1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply dithered adjustment and clamp.
|
||||
const int rate_shift = 1 + (rate > 59) + (rate > 55);
|
||||
attenuation_ += dithering_patterns[rate & 3][(oscillator.counter >> shift_size) & 7] * (4 << rate_shift);
|
||||
attenuation_ = std::min(attenuation_, 511);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -11,42 +11,42 @@
|
||||
namespace Yamaha::OPL {
|
||||
|
||||
template <int frequency_precision> class KeyLevelScaler {
|
||||
public:
|
||||
public:
|
||||
|
||||
/*!
|
||||
Sets the current period associated with the channel that owns this envelope generator;
|
||||
this is used to select a key scaling rate if key-rate scaling is enabled.
|
||||
*/
|
||||
void set_period(int period, int octave) {
|
||||
constexpr int key_level_scales[16] = {0, 48, 64, 74, 80, 86, 90, 94, 96, 100, 102, 104, 106, 108, 110, 112};
|
||||
constexpr int masks[2] = {~0, 0};
|
||||
/*!
|
||||
Sets the current period associated with the channel that owns this envelope generator;
|
||||
this is used to select a key scaling rate if key-rate scaling is enabled.
|
||||
*/
|
||||
void set_period(const int period, const int octave) {
|
||||
constexpr int key_level_scales[16] = {0, 48, 64, 74, 80, 86, 90, 94, 96, 100, 102, 104, 106, 108, 110, 112};
|
||||
constexpr int masks[2] = {~0, 0};
|
||||
|
||||
// A two's complement assumption is embedded below; the use of masks relies
|
||||
// on the sign bit to clamp to zero.
|
||||
level_ = key_level_scales[period >> (frequency_precision - 4)];
|
||||
level_ -= 16 * (octave ^ 7);
|
||||
level_ &= masks[(level_ >> ((sizeof(int) * 8) - 1)) & 1];
|
||||
}
|
||||
// A two's complement assumption is embedded below; the use of masks relies
|
||||
// on the sign bit to clamp to zero.
|
||||
level_ = key_level_scales[period >> (frequency_precision - 4)];
|
||||
level_ -= 16 * (octave ^ 7);
|
||||
level_ &= masks[(level_ >> ((sizeof(int) * 8) - 1)) & 1];
|
||||
}
|
||||
|
||||
/*!
|
||||
Enables or disables key-rate scaling.
|
||||
*/
|
||||
void set_key_scaling_level(int level) {
|
||||
// '7' is just a number large enough to render all possible scaling coefficients as 0.
|
||||
constexpr int key_level_scale_shifts[4] = {7, 1, 2, 0};
|
||||
shift_ = key_level_scale_shifts[level];
|
||||
}
|
||||
/*!
|
||||
Enables or disables key-rate scaling.
|
||||
*/
|
||||
void set_key_scaling_level(const int level) {
|
||||
// '7' is just a number large enough to render all possible scaling coefficients as 0.
|
||||
constexpr int key_level_scale_shifts[4] = {7, 1, 2, 0};
|
||||
shift_ = key_level_scale_shifts[level];
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The current attenuation level due to key-level scaling.
|
||||
*/
|
||||
int attenuation() const {
|
||||
return level_ >> shift_;
|
||||
}
|
||||
/*!
|
||||
@returns The current attenuation level due to key-level scaling.
|
||||
*/
|
||||
int attenuation() const {
|
||||
return level_ >> shift_;
|
||||
}
|
||||
|
||||
private:
|
||||
int level_ = 0;
|
||||
int shift_ = 0;
|
||||
private:
|
||||
int level_ = 0;
|
||||
int shift_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -18,46 +18,46 @@ namespace Yamaha::OPL {
|
||||
as part of their ADSR envelope.
|
||||
*/
|
||||
class LowFrequencyOscillator {
|
||||
public:
|
||||
/// Current attenuation due to tremolo / amplitude modulation, between 0 and 26.
|
||||
int tremolo = 0;
|
||||
public:
|
||||
/// Current attenuation due to tremolo / amplitude modulation, between 0 and 26.
|
||||
int tremolo = 0;
|
||||
|
||||
/// A number between 0 and 7 indicating the current vibrato offset; this should be combined by operators
|
||||
/// with their frequency number to get the actual vibrato.
|
||||
int vibrato = 0;
|
||||
/// A number between 0 and 7 indicating the current vibrato offset; this should be combined by operators
|
||||
/// with their frequency number to get the actual vibrato.
|
||||
int vibrato = 0;
|
||||
|
||||
/// A counter of the number of operator update cycles (i.e. input clock / 72) since an arbitrary time.
|
||||
int counter = 0;
|
||||
/// A counter of the number of operator update cycles (i.e. input clock / 72) since an arbitrary time.
|
||||
int counter = 0;
|
||||
|
||||
/// Describes the current output of the LFSR; will be either 0 or 1.
|
||||
int lfsr = 0;
|
||||
/// Describes the current output of the LFSR; will be either 0 or 1.
|
||||
int lfsr = 0;
|
||||
|
||||
/// Updates the oscillator outputs. Should be called at the (input clock/72) rate.
|
||||
void update() {
|
||||
++counter;
|
||||
/// Updates the oscillator outputs. Should be called at the (input clock/72) rate.
|
||||
void update() {
|
||||
++counter;
|
||||
|
||||
// This produces output of:
|
||||
//
|
||||
// four instances of 0, four instances of 1... _three_ instances of 26,
|
||||
// four instances of 25, four instances of 24... _three_ instances of 0.
|
||||
//
|
||||
// ... advancing once every 64th update.
|
||||
const int tremolo_index = (counter >> 6) % 210;
|
||||
const int tremolo_levels[2] = {tremolo_index >> 2, 52 - ((tremolo_index+1) >> 2)};
|
||||
tremolo = tremolo_levels[tremolo_index / 107];
|
||||
// This produces output of:
|
||||
//
|
||||
// four instances of 0, four instances of 1... _three_ instances of 26,
|
||||
// four instances of 25, four instances of 24... _three_ instances of 0.
|
||||
//
|
||||
// ... advancing once every 64th update.
|
||||
const int tremolo_index = (counter >> 6) % 210;
|
||||
const int tremolo_levels[2] = {tremolo_index >> 2, 52 - ((tremolo_index+1) >> 2)};
|
||||
tremolo = tremolo_levels[tremolo_index / 107];
|
||||
|
||||
// Vibrato is relatively simple: it's just three bits from the counter.
|
||||
vibrato = (counter >> 10) & 7;
|
||||
}
|
||||
// Vibrato is relatively simple: it's just three bits from the counter.
|
||||
vibrato = (counter >> 10) & 7;
|
||||
}
|
||||
|
||||
/// Updartes the LFSR output. Should be called at the input clock rate.
|
||||
void update_lfsr() {
|
||||
lfsr = noise_source_.next();
|
||||
}
|
||||
/// Updartes the LFSR output. Should be called at the input clock rate.
|
||||
void update_lfsr() {
|
||||
lfsr = noise_source_.next();
|
||||
}
|
||||
|
||||
private:
|
||||
// This is the correct LSFR per forums.submarine.org.uk.
|
||||
Numeric::LFSR<int, 0x800302> noise_source_;
|
||||
private:
|
||||
// This is the correct LSFR per forums.submarine.org.uk.
|
||||
Numeric::LFSR<int, 0x800302> noise_source_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -14,22 +14,22 @@
|
||||
namespace Yamaha::OPL {
|
||||
|
||||
template <typename Child, bool stereo> class OPLBase: public ::Outputs::Speaker::BufferSource<Child, stereo> {
|
||||
public:
|
||||
void write(uint16_t address, uint8_t value) {
|
||||
if(address & 1) {
|
||||
static_cast<Child *>(this)->write_register(selected_register_, value);
|
||||
} else {
|
||||
selected_register_ = value;
|
||||
}
|
||||
public:
|
||||
void write(const uint16_t address, const uint8_t value) {
|
||||
if(address & 1) {
|
||||
static_cast<Child *>(this)->write_register(selected_register_, value);
|
||||
} else {
|
||||
selected_register_ = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
OPLBase(Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) {}
|
||||
protected:
|
||||
OPLBase(Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) {}
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
|
||||
private:
|
||||
uint8_t selected_register_ = 0;
|
||||
private:
|
||||
uint8_t selected_register_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -19,102 +19,102 @@ namespace Yamaha::OPL {
|
||||
multiple, and whether to apply vibrato, this will then appropriately update and return phase.
|
||||
*/
|
||||
template <int precision> class PhaseGenerator {
|
||||
public:
|
||||
/*!
|
||||
Advances the phase generator a single step, given the current state of the low-frequency oscillator, @c oscillator.
|
||||
*/
|
||||
void update(const LowFrequencyOscillator &oscillator) {
|
||||
constexpr int vibrato_shifts[4] = {3, 1, 0, 1};
|
||||
constexpr int vibrato_signs[2] = {1, -1};
|
||||
public:
|
||||
/*!
|
||||
Advances the phase generator a single step, given the current state of the low-frequency oscillator, @c oscillator.
|
||||
*/
|
||||
void update(const LowFrequencyOscillator &oscillator) {
|
||||
constexpr int vibrato_shifts[4] = {3, 1, 0, 1};
|
||||
constexpr int vibrato_signs[2] = {1, -1};
|
||||
|
||||
// Get just the top three bits of the period_.
|
||||
const int top_freq = period_ >> (precision - 3);
|
||||
// Get just the top three bits of the period_.
|
||||
const int top_freq = period_ >> (precision - 3);
|
||||
|
||||
// Cacluaute applicable vibrato as a function of (i) the top three bits of the
|
||||
// oscillator period; (ii) the current low-frequency oscillator vibrato output; and
|
||||
// (iii) whether vibrato is enabled.
|
||||
const int vibrato = (top_freq >> vibrato_shifts[oscillator.vibrato & 3]) * vibrato_signs[oscillator.vibrato >> 2] * enable_vibrato_;
|
||||
// Cacluaute applicable vibrato as a function of (i) the top three bits of the
|
||||
// oscillator period; (ii) the current low-frequency oscillator vibrato output; and
|
||||
// (iii) whether vibrato is enabled.
|
||||
const int vibrato = (top_freq >> vibrato_shifts[oscillator.vibrato & 3]) * vibrato_signs[oscillator.vibrato >> 2] * enable_vibrato_;
|
||||
|
||||
// Apply phase update with vibrato from the low-frequency oscillator.
|
||||
phase_ += (multiple_ * ((period_ << 1) + vibrato) << octave_) >> 1;
|
||||
}
|
||||
// Apply phase update with vibrato from the low-frequency oscillator.
|
||||
phase_ += (multiple_ * ((period_ << 1) + vibrato) << octave_) >> 1;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
@returns Current phase; real hardware provides only the low ten bits of this result.
|
||||
*/
|
||||
int phase() const {
|
||||
// My table if multipliers is multiplied by two, so shift by one more
|
||||
// than the stated precision.
|
||||
return phase_ >> precision_shift;
|
||||
}
|
||||
/*!
|
||||
@returns Current phase; real hardware provides only the low ten bits of this result.
|
||||
*/
|
||||
int phase() const {
|
||||
// My table if multipliers is multiplied by two, so shift by one more
|
||||
// than the stated precision.
|
||||
return phase_ >> precision_shift;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns Current phase, scaled up by (1 << precision).
|
||||
*/
|
||||
int scaled_phase() const {
|
||||
return phase_ >> 1;
|
||||
}
|
||||
/*!
|
||||
@returns Current phase, scaled up by (1 << precision).
|
||||
*/
|
||||
int scaled_phase() const {
|
||||
return phase_ >> 1;
|
||||
}
|
||||
|
||||
/*!
|
||||
Applies feedback based on two historic samples of a total output level,
|
||||
plus the degree of feedback to apply
|
||||
*/
|
||||
void apply_feedback(LogSign first, LogSign second, int level) {
|
||||
constexpr int masks[] = {0, ~0, ~0, ~0, ~0, ~0, ~0, ~0};
|
||||
phase_ += ((second.level(precision) + first.level(precision)) >> (8 - level)) & masks[level];
|
||||
}
|
||||
/*!
|
||||
Applies feedback based on two historic samples of a total output level,
|
||||
plus the degree of feedback to apply
|
||||
*/
|
||||
void apply_feedback(const LogSign first, const LogSign second, const int level) {
|
||||
constexpr int masks[] = {0, ~0, ~0, ~0, ~0, ~0, ~0, ~0};
|
||||
phase_ += ((second.level(precision) + first.level(precision)) >> (8 - level)) & masks[level];
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the multiple for this phase generator, in the same terms as an OPL programmer,
|
||||
i.e. a 4-bit number that is used as a lookup into the internal multiples table.
|
||||
*/
|
||||
void set_multiple(int multiple) {
|
||||
// This encodes the MUL -> multiple table given on page 12,
|
||||
// multiplied by two.
|
||||
constexpr int multipliers[] = {
|
||||
1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 20, 24, 24, 30, 30
|
||||
};
|
||||
assert(multiple < 16);
|
||||
multiple_ = multipliers[multiple];
|
||||
}
|
||||
/*!
|
||||
Sets the multiple for this phase generator, in the same terms as an OPL programmer,
|
||||
i.e. a 4-bit number that is used as a lookup into the internal multiples table.
|
||||
*/
|
||||
void set_multiple(const int multiple) {
|
||||
// This encodes the MUL -> multiple table given on page 12,
|
||||
// multiplied by two.
|
||||
constexpr int multipliers[] = {
|
||||
1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 20, 24, 24, 30, 30
|
||||
};
|
||||
assert(multiple < 16);
|
||||
multiple_ = multipliers[multiple];
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the period of this generator, along with its current octave.
|
||||
/*!
|
||||
Sets the period of this generator, along with its current octave.
|
||||
|
||||
Yamaha tends to refer to the period as the 'f-number', and used both 'octave' and 'block' for octave.
|
||||
*/
|
||||
void set_period(int period, int octave) {
|
||||
period_ = period;
|
||||
octave_ = octave;
|
||||
Yamaha tends to refer to the period as the 'f-number', and used both 'octave' and 'block' for octave.
|
||||
*/
|
||||
void set_period(const int period, const int octave) {
|
||||
period_ = period;
|
||||
octave_ = octave;
|
||||
|
||||
assert(octave_ < 8);
|
||||
assert(period_ < (1 << precision));
|
||||
}
|
||||
assert(octave_ < 8);
|
||||
assert(period_ < (1 << precision));
|
||||
}
|
||||
|
||||
/*!
|
||||
Enables or disables vibrato.
|
||||
*/
|
||||
void set_vibrato_enabled(bool enabled) {
|
||||
enable_vibrato_ = int(enabled);
|
||||
}
|
||||
/*!
|
||||
Enables or disables vibrato.
|
||||
*/
|
||||
void set_vibrato_enabled(const bool enabled) {
|
||||
enable_vibrato_ = int(enabled);
|
||||
}
|
||||
|
||||
/*!
|
||||
Resets the current phase.
|
||||
*/
|
||||
void reset() {
|
||||
phase_ = 0;
|
||||
}
|
||||
/*!
|
||||
Resets the current phase.
|
||||
*/
|
||||
void reset() {
|
||||
phase_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int precision_shift = 1 + precision;
|
||||
private:
|
||||
static constexpr int precision_shift = 1 + precision;
|
||||
|
||||
int phase_ = 0;
|
||||
int phase_ = 0;
|
||||
|
||||
int multiple_ = 0;
|
||||
int period_ = 0;
|
||||
int octave_ = 0;
|
||||
int enable_vibrato_ = 0;
|
||||
int multiple_ = 0;
|
||||
int period_ = 0;
|
||||
int octave_ = 0;
|
||||
int enable_vibrato_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -32,12 +32,12 @@ struct LogSign {
|
||||
sign = 1;
|
||||
}
|
||||
|
||||
LogSign &operator +=(int attenuation) {
|
||||
LogSign &operator +=(const int attenuation) {
|
||||
log += attenuation;
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogSign &operator +=(LogSign log_sign) {
|
||||
LogSign &operator +=(const LogSign log_sign) {
|
||||
log += log_sign.log;
|
||||
sign *= log_sign.sign;
|
||||
return *this;
|
||||
@ -49,7 +49,7 @@ struct LogSign {
|
||||
/*!
|
||||
@returns Negative log sin of x, assuming a 1024-unit circle.
|
||||
*/
|
||||
constexpr LogSign negative_log_sin(int x) {
|
||||
constexpr LogSign negative_log_sin(const int x) {
|
||||
/// Defines the first quadrant of 1024-unit negative log to the base two of sine (that conveniently misses sin(0)).
|
||||
///
|
||||
/// Expected branchless usage for a full 1024 unit output:
|
||||
@ -107,7 +107,7 @@ constexpr LogSign negative_log_sin(int x) {
|
||||
Computes the linear value represented by the log-sign @c ls, shifted left by @c fractional prior
|
||||
to loss of precision.
|
||||
*/
|
||||
constexpr int power_two(LogSign ls, int fractional = 0) {
|
||||
constexpr int power_two(const LogSign ls, const int fractional = 0) {
|
||||
/// A derivative of the exponent table in a real OPL2; mapped_exp[x] = (source[c ^ 0xff] << 1) | 0x800.
|
||||
///
|
||||
/// The ahead-of-time transformation represents fixed work the OPL2 does when reading its table
|
||||
@ -215,7 +215,7 @@ constexpr uint8_t percussion_patch_set[] = {
|
||||
};
|
||||
|
||||
|
||||
inline int LogSign::level(int fractional) const {
|
||||
inline int LogSign::level(const int fractional) const {
|
||||
return power_two(*this, fractional);
|
||||
}
|
||||
|
||||
|
@ -18,70 +18,74 @@ enum class Waveform {
|
||||
};
|
||||
|
||||
template <int phase_precision> class WaveformGenerator {
|
||||
public:
|
||||
/*!
|
||||
@returns The output of waveform @c form at [integral] phase @c phase.
|
||||
*/
|
||||
static constexpr LogSign wave(Waveform form, int phase) {
|
||||
constexpr int waveforms[4][4] = {
|
||||
{1023, 1023, 1023, 1023}, // Sine: don't mask in any quadrant.
|
||||
{511, 511, 0, 0}, // Half sine: keep the first half intact, lock to 0 in the second half.
|
||||
{511, 511, 511, 511}, // AbsSine: endlessly repeat the first half of the sine wave.
|
||||
{255, 0, 255, 0}, // PulseSine: act as if the first quadrant is in the first and third; lock the other two to 0.
|
||||
};
|
||||
return negative_log_sin(phase & waveforms[int(form)][(phase >> 8) & 3]);
|
||||
}
|
||||
public:
|
||||
/*!
|
||||
@returns The output of waveform @c form at [integral] phase @c phase.
|
||||
*/
|
||||
static constexpr LogSign wave(Waveform form, int phase) {
|
||||
constexpr int waveforms[4][4] = {
|
||||
{1023, 1023, 1023, 1023}, // Sine: don't mask in any quadrant.
|
||||
{511, 511, 0, 0}, // Half sine: keep the first half intact, lock to 0 in the second half.
|
||||
{511, 511, 511, 511}, // AbsSine: endlessly repeat the first half of the sine wave.
|
||||
{255, 0, 255, 0}, // PulseSine: act as if the first quadrant is in the first and third; lock the other two to 0.
|
||||
};
|
||||
return negative_log_sin(phase & waveforms[int(form)][(phase >> 8) & 3]);
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The output of waveform @c form at [scaled] phase @c scaled_phase given the modulation input @c modulation.
|
||||
*/
|
||||
static constexpr LogSign wave(Waveform form, int scaled_phase, LogSign modulation) {
|
||||
const int scaled_phase_offset = modulation.level(phase_precision);
|
||||
const int phase = (scaled_phase + scaled_phase_offset) >> phase_precision;
|
||||
return wave(form, phase);
|
||||
}
|
||||
/*!
|
||||
@returns The output of waveform @c form at [scaled] phase @c scaled_phase given the modulation input @c modulation.
|
||||
*/
|
||||
static constexpr LogSign wave(const Waveform form, const int scaled_phase, const LogSign modulation) {
|
||||
const int scaled_phase_offset = modulation.level(phase_precision);
|
||||
const int phase = (scaled_phase + scaled_phase_offset) >> phase_precision;
|
||||
return wave(form, phase);
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns Snare output, calculated from the current LFSR state as captured in @c oscillator and an operator's phase.
|
||||
*/
|
||||
static constexpr LogSign snare(const LowFrequencyOscillator &oscillator, int phase) {
|
||||
// If noise is 0, output is positive.
|
||||
// If noise is 1, output is negative.
|
||||
// If (noise ^ sign) is 0, output is 0. Otherwise it is max.
|
||||
const int sign = phase & 0x200;
|
||||
const int level = ((phase >> 9) & 1) ^ oscillator.lfsr;
|
||||
return negative_log_sin(sign + (level << 8));
|
||||
}
|
||||
/*!
|
||||
@returns Snare output, calculated from the current LFSR state as captured in @c oscillator and an operator's phase.
|
||||
*/
|
||||
static constexpr LogSign snare(const LowFrequencyOscillator &oscillator, const int phase) {
|
||||
// If noise is 0, output is positive.
|
||||
// If noise is 1, output is negative.
|
||||
// If (noise ^ sign) is 0, output is 0. Otherwise it is max.
|
||||
const int sign = phase & 0x200;
|
||||
const int level = ((phase >> 9) & 1) ^ oscillator.lfsr;
|
||||
return negative_log_sin(sign + (level << 8));
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns Cymbal output, calculated from an operator's phase and a modulator's phase.
|
||||
*/
|
||||
static constexpr LogSign cymbal(int carrier_phase, int modulator_phase) {
|
||||
return negative_log_sin(256 + (phase_combination(carrier_phase, modulator_phase) << 9));
|
||||
}
|
||||
/*!
|
||||
@returns Cymbal output, calculated from an operator's phase and a modulator's phase.
|
||||
*/
|
||||
static constexpr LogSign cymbal(const int carrier_phase, const int modulator_phase) {
|
||||
return negative_log_sin(256 + (phase_combination(carrier_phase, modulator_phase) << 9));
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns High-hat output, calculated from the current LFSR state as captured in @c oscillator, an operator's phase and a modulator's phase.
|
||||
*/
|
||||
static constexpr LogSign high_hat(const LowFrequencyOscillator &oscillator, int carrier_phase, int modulator_phase) {
|
||||
constexpr int angles[] = {0x234, 0xd0, 0x2d0, 0x34};
|
||||
return negative_log_sin(angles[
|
||||
phase_combination(carrier_phase, modulator_phase) |
|
||||
(oscillator.lfsr << 1)
|
||||
]);
|
||||
}
|
||||
/*!
|
||||
@returns High-hat output, calculated from the current LFSR state as captured in @c oscillator, an operator's phase and a modulator's phase.
|
||||
*/
|
||||
static constexpr LogSign high_hat(
|
||||
const LowFrequencyOscillator &oscillator,
|
||||
const int carrier_phase,
|
||||
const int modulator_phase
|
||||
) {
|
||||
constexpr int angles[] = {0x234, 0xd0, 0x2d0, 0x34};
|
||||
return negative_log_sin(angles[
|
||||
phase_combination(carrier_phase, modulator_phase) |
|
||||
(oscillator.lfsr << 1)
|
||||
]);
|
||||
}
|
||||
|
||||
private:
|
||||
/*!
|
||||
@returns The phase bit used for cymbal and high-hat generation, which is a function of two operators' phases.
|
||||
*/
|
||||
static constexpr int phase_combination(int carrier_phase, int modulator_phase) {
|
||||
return (
|
||||
((carrier_phase >> 5) ^ (carrier_phase >> 3)) &
|
||||
((modulator_phase >> 7) ^ (modulator_phase >> 2)) &
|
||||
((carrier_phase >> 5) ^ (modulator_phase >> 3))
|
||||
) & 1;
|
||||
}
|
||||
private:
|
||||
/*!
|
||||
@returns The phase bit used for cymbal and high-hat generation, which is a function of two operators' phases.
|
||||
*/
|
||||
static constexpr int phase_combination(const int carrier_phase, const int modulator_phase) {
|
||||
return (
|
||||
((carrier_phase >> 5) ^ (carrier_phase >> 3)) &
|
||||
((modulator_phase >> 7) ^ (modulator_phase >> 2)) &
|
||||
((carrier_phase >> 5) ^ (modulator_phase >> 3))
|
||||
) & 1;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
using namespace Yamaha::OPL;
|
||||
|
||||
OPLL::OPLL(Concurrency::AsyncTaskQueue<false> &task_queue, int audio_divider, bool is_vrc7):
|
||||
OPLL::OPLL(Concurrency::AsyncTaskQueue<false> &task_queue, const int audio_divider, const bool is_vrc7):
|
||||
OPLBase(task_queue), audio_divider_(audio_divider), is_vrc7_(is_vrc7) {
|
||||
// Due to the way that sound mixing works on the OPLL, the audio divider may not
|
||||
// be larger than 4.
|
||||
@ -71,7 +71,7 @@ OPLL::OPLL(Concurrency::AsyncTaskQueue<false> &task_queue, int audio_divider, bo
|
||||
|
||||
// MARK: - Machine-facing programmatic input.
|
||||
|
||||
void OPLL::write_register(uint8_t address, uint8_t value) {
|
||||
void OPLL::write_register(const uint8_t address, const uint8_t value) {
|
||||
// The OPLL doesn't have timers or other non-audio functions, so all writes
|
||||
// go to the audio queue.
|
||||
task_queue_.enqueue([this, address, value] {
|
||||
@ -172,7 +172,7 @@ void OPLL::write_register(uint8_t address, uint8_t value) {
|
||||
});
|
||||
}
|
||||
|
||||
void OPLL::set_channel_period(int channel) {
|
||||
void OPLL::set_channel_period(const int channel) {
|
||||
phase_generators_[channel + 0].set_period(channels_[channel].period, channels_[channel].octave);
|
||||
phase_generators_[channel + 9].set_period(channels_[channel].period, channels_[channel].octave);
|
||||
|
||||
@ -183,7 +183,7 @@ void OPLL::set_channel_period(int channel) {
|
||||
key_level_scalers_[channel + 9].set_period(channels_[channel].period, channels_[channel].octave);
|
||||
}
|
||||
|
||||
const uint8_t *OPLL::instrument_definition(int instrument, int channel) {
|
||||
const uint8_t *OPLL::instrument_definition(const int instrument, const int channel) const {
|
||||
// Divert to the appropriate rhythm instrument if in rhythm mode.
|
||||
if(channel >= 6 && rhythm_mode_enabled_) {
|
||||
return &percussion_patch_set[(channel - 6) * 8];
|
||||
@ -197,7 +197,7 @@ const uint8_t *OPLL::instrument_definition(int instrument, int channel) {
|
||||
return is_vrc7_ ? &vrc7_patch_set[index] : &opll_patch_set[index];
|
||||
}
|
||||
|
||||
void OPLL::install_instrument(int channel) {
|
||||
void OPLL::install_instrument(const int channel) {
|
||||
auto &carrier_envelope = envelope_generators_[channel + 0];
|
||||
auto &carrier_phase = phase_generators_[channel + 0];
|
||||
auto &carrier_scaler = key_level_scalers_[channel + 0];
|
||||
@ -266,7 +266,7 @@ void OPLL::install_instrument(int channel) {
|
||||
carrier_envelope.set_sustain_level(instrument[7] >> 4);
|
||||
}
|
||||
|
||||
void OPLL::set_use_sustain(int channel) {
|
||||
void OPLL::set_use_sustain(const int channel) {
|
||||
const uint8_t *const instrument = instrument_definition(channels_[channel].instrument, channel);
|
||||
envelope_generators_[channel + 0].set_use_sustain_level((instrument[1] & 0x20) || channels_[channel].use_sustain);
|
||||
envelope_generators_[channel + 9].set_use_sustain_level((instrument[0] & 0x20) || channels_[channel].use_sustain);
|
||||
@ -274,7 +274,7 @@ void OPLL::set_use_sustain(int channel) {
|
||||
|
||||
// MARK: - Output generation.
|
||||
|
||||
void OPLL::set_sample_volume_range(std::int16_t range) {
|
||||
void OPLL::set_sample_volume_range(const std::int16_t range) {
|
||||
total_volume_ = range;
|
||||
}
|
||||
|
||||
@ -387,7 +387,7 @@ void OPLL::update_all_channels() {
|
||||
|
||||
#define ATTENUATION(x) ((x) << 7)
|
||||
|
||||
int OPLL::melodic_output(int channel) {
|
||||
int OPLL::melodic_output(const int channel) {
|
||||
// The modulator always updates after the carrier, oddly enough. So calculate actual output first, based on the modulator's last value.
|
||||
auto carrier = WaveformGenerator<period_precision>::wave(channels_[channel].carrier_waveform, phase_generators_[channel].scaled_phase(), channels_[channel].modulator_output);
|
||||
carrier += envelope_generators_[channel].attenuation() + ATTENUATION(channels_[channel].attenuation) + key_level_scalers_[channel].attenuation();
|
||||
@ -403,7 +403,7 @@ int OPLL::melodic_output(int channel) {
|
||||
return carrier.level();
|
||||
}
|
||||
|
||||
int OPLL::bass_drum() {
|
||||
int OPLL::bass_drum() const {
|
||||
// Use modulator 6 and carrier 6, attenuated as per the bass-specific envelope generators and the attenuation level for channel 6.
|
||||
auto modulation = WaveformGenerator<period_precision>::wave(Waveform::Sine, phase_generators_[6 + 9].phase());
|
||||
modulation += rhythm_envelope_generators_[RhythmIndices::BassModulator].attenuation();
|
||||
@ -413,7 +413,7 @@ int OPLL::bass_drum() {
|
||||
return carrier.level();
|
||||
}
|
||||
|
||||
int OPLL::tom_tom() {
|
||||
int OPLL::tom_tom() const {
|
||||
// Use modulator 8 and the 'instrument' selection for channel 8 as an attenuation.
|
||||
auto tom_tom = WaveformGenerator<period_precision>::wave(Waveform::Sine, phase_generators_[8 + 9].phase());
|
||||
tom_tom += rhythm_envelope_generators_[RhythmIndices::TomTom].attenuation();
|
||||
@ -421,7 +421,7 @@ int OPLL::tom_tom() {
|
||||
return tom_tom.level();
|
||||
}
|
||||
|
||||
int OPLL::snare_drum() {
|
||||
int OPLL::snare_drum() const {
|
||||
// Use modulator 7 and the carrier attenuation level for channel 7.
|
||||
LogSign snare = WaveformGenerator<period_precision>::snare(oscillator_, phase_generators_[7 + 9].phase());
|
||||
snare += rhythm_envelope_generators_[RhythmIndices::Snare].attenuation();
|
||||
@ -429,7 +429,7 @@ int OPLL::snare_drum() {
|
||||
return snare.level();
|
||||
}
|
||||
|
||||
int OPLL::cymbal() {
|
||||
int OPLL::cymbal() const {
|
||||
// Use modulator 7, carrier 8 and the attenuation level for channel 8.
|
||||
LogSign cymbal = WaveformGenerator<period_precision>::cymbal(phase_generators_[8].phase(), phase_generators_[7 + 9].phase());
|
||||
cymbal += rhythm_envelope_generators_[RhythmIndices::Cymbal].attenuation();
|
||||
@ -437,7 +437,7 @@ int OPLL::cymbal() {
|
||||
return cymbal.level();
|
||||
}
|
||||
|
||||
int OPLL::high_hat() {
|
||||
int OPLL::high_hat() const {
|
||||
// Use modulator 7, carrier 8 a and the 'instrument' selection for channel 7 as an attenuation.
|
||||
LogSign high_hat = WaveformGenerator<period_precision>::high_hat(oscillator_, phase_generators_[8].phase(), phase_generators_[7 + 9].phase());
|
||||
high_hat += rhythm_envelope_generators_[RhythmIndices::HighHat].attenuation();
|
||||
|
@ -26,7 +26,7 @@ class OPLL: public OPLBase<OPLL, false> {
|
||||
|
||||
/// As per ::SampleSource; provides audio output.
|
||||
template <Outputs::Speaker::Action action>
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
|
||||
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
bool is_zero_level() const { return false; } // TODO.
|
||||
|
||||
@ -49,11 +49,11 @@ class OPLL: public OPLBase<OPLL, false> {
|
||||
void update_all_channels();
|
||||
|
||||
int melodic_output(int channel);
|
||||
int bass_drum();
|
||||
int tom_tom();
|
||||
int snare_drum();
|
||||
int cymbal();
|
||||
int high_hat();
|
||||
int bass_drum() const;
|
||||
int tom_tom() const;
|
||||
int snare_drum() const;
|
||||
int cymbal() const;
|
||||
int high_hat() const;
|
||||
|
||||
static constexpr int period_precision = 9;
|
||||
static constexpr int envelope_precision = 7;
|
||||
@ -122,7 +122,7 @@ class OPLL: public OPLBase<OPLL, false> {
|
||||
void set_use_sustain(int channel);
|
||||
|
||||
/// @returns The 8-byte definition of instrument @c instrument.
|
||||
const uint8_t *instrument_definition(int instrument, int channel);
|
||||
const uint8_t *instrument_definition(int instrument, int channel) const;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ RP5C01::RP5C01(HalfCycles clock_rate) : clock_rate_(clock_rate) {
|
||||
leap_year_ = time_date->tm_year % 4;
|
||||
}
|
||||
|
||||
void RP5C01::run_for(HalfCycles cycles) {
|
||||
void RP5C01::run_for(const HalfCycles cycles) {
|
||||
sub_seconds_ += cycles;
|
||||
|
||||
// Guess: this happens so rarely (i.e. once a second, ordinarily) that
|
||||
@ -96,13 +96,13 @@ void RP5C01::run_for(HalfCycles cycles) {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int Reg(int mode, int address) {
|
||||
constexpr int Reg(const int mode, const int address) {
|
||||
return address | mode << 4;
|
||||
}
|
||||
|
||||
constexpr int PM = 1 << 4;
|
||||
|
||||
constexpr int twenty_four_to_twelve(int hours) {
|
||||
constexpr int twenty_four_to_twelve(const int hours) {
|
||||
switch(hours) {
|
||||
default: return (hours % 12) + (hours > 12 ? PM : 0);
|
||||
case 0: return 12;
|
||||
|
@ -16,41 +16,41 @@
|
||||
namespace Ricoh::RP5C01 {
|
||||
|
||||
class RP5C01 {
|
||||
public:
|
||||
RP5C01(HalfCycles clock_rate);
|
||||
public:
|
||||
RP5C01(HalfCycles clock_rate);
|
||||
|
||||
/// @returns the result of a read from @c address.
|
||||
uint8_t read(int address);
|
||||
/// @returns the result of a read from @c address.
|
||||
uint8_t read(int address);
|
||||
|
||||
/// Performs a write of @c value to @c address.
|
||||
void write(int address, uint8_t value);
|
||||
/// Performs a write of @c value to @c address.
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/// Advances time.
|
||||
void run_for(HalfCycles);
|
||||
/// Advances time.
|
||||
void run_for(HalfCycles);
|
||||
|
||||
private:
|
||||
std::array<uint8_t, 26> ram_;
|
||||
private:
|
||||
std::array<uint8_t, 26> ram_;
|
||||
|
||||
HalfCycles sub_seconds_;
|
||||
const HalfCycles clock_rate_;
|
||||
HalfCycles sub_seconds_;
|
||||
const HalfCycles clock_rate_;
|
||||
|
||||
// Contains the seconds, minutes and hours fields.
|
||||
int seconds_ = 0;
|
||||
// Contains the seconds, minutes and hours fields.
|
||||
int seconds_ = 0;
|
||||
|
||||
// Calendar entries.
|
||||
int day_of_the_week_ = 0;
|
||||
int day_ = 0;
|
||||
int month_ = 0;
|
||||
int year_ = 0;
|
||||
int leap_year_ = 0;
|
||||
// Calendar entries.
|
||||
int day_of_the_week_ = 0;
|
||||
int day_ = 0;
|
||||
int month_ = 0;
|
||||
int year_ = 0;
|
||||
int leap_year_ = 0;
|
||||
|
||||
// Other flags.
|
||||
bool timer_enabled_ = false;
|
||||
bool alarm_enabled_ = false;
|
||||
int mode_ = 0;
|
||||
bool one_hz_on_ = false;
|
||||
bool sixteen_hz_on_ = false;
|
||||
bool twentyfour_hour_clock_ = true;
|
||||
// Other flags.
|
||||
bool timer_enabled_ = false;
|
||||
bool alarm_enabled_ = false;
|
||||
int mode_ = 0;
|
||||
bool one_hz_on_ = false;
|
||||
bool sixteen_hz_on_ = false;
|
||||
bool twentyfour_hour_clock_ = true;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -13,7 +13,11 @@
|
||||
|
||||
using namespace TI;
|
||||
|
||||
SN76489::SN76489(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue, int additional_divider) : task_queue_(task_queue) {
|
||||
SN76489::SN76489(
|
||||
const Personality personality,
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue,
|
||||
const int additional_divider
|
||||
) : task_queue_(task_queue) {
|
||||
set_sample_volume_range(0);
|
||||
|
||||
switch(personality) {
|
||||
@ -36,7 +40,7 @@ SN76489::SN76489(Personality personality, Concurrency::AsyncTaskQueue<false> &ta
|
||||
master_divider_period_ /= additional_divider;
|
||||
}
|
||||
|
||||
void SN76489::set_sample_volume_range(std::int16_t range) {
|
||||
void SN76489::set_sample_volume_range(const std::int16_t range) {
|
||||
// Build a volume table.
|
||||
double multiplier = pow(10.0, -0.1);
|
||||
double volume = float(range) / 4.0f; // As there are four channels.
|
||||
@ -48,7 +52,7 @@ void SN76489::set_sample_volume_range(std::int16_t range) {
|
||||
evaluate_output_volume();
|
||||
}
|
||||
|
||||
void SN76489::write(uint8_t value) {
|
||||
void SN76489::write(const uint8_t value) {
|
||||
task_queue_.enqueue([value, this] () {
|
||||
if(value & 0x80) {
|
||||
active_register_ = value;
|
||||
@ -87,7 +91,11 @@ void SN76489::write(uint8_t value) {
|
||||
}
|
||||
|
||||
bool SN76489::is_zero_level() const {
|
||||
return channels_[0].volume == 0xf && channels_[1].volume == 0xf && channels_[2].volume == 0xf && channels_[3].volume == 0xf;
|
||||
return
|
||||
channels_[0].volume == 0xf &&
|
||||
channels_[1].volume == 0xf &&
|
||||
channels_[2].volume == 0xf &&
|
||||
channels_[3].volume == 0xf;
|
||||
}
|
||||
|
||||
void SN76489::evaluate_output_volume() {
|
||||
@ -100,7 +108,7 @@ void SN76489::evaluate_output_volume() {
|
||||
}
|
||||
|
||||
template <Outputs::Speaker::Action action>
|
||||
void SN76489::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
|
||||
void SN76489::apply_samples(const std::size_t number_of_samples, Outputs::Speaker::MonoSample *const target) {
|
||||
std::size_t c = 0;
|
||||
while((master_divider_& (master_divider_period_ - 1)) && c < number_of_samples) {
|
||||
Outputs::Speaker::apply<action>(target[c], output_volume_);
|
||||
|
@ -14,53 +14,53 @@
|
||||
namespace TI {
|
||||
|
||||
class SN76489: public Outputs::Speaker::BufferSource<SN76489, false> {
|
||||
public:
|
||||
enum class Personality {
|
||||
SN76489,
|
||||
SN76494,
|
||||
SMS
|
||||
};
|
||||
public:
|
||||
enum class Personality {
|
||||
SN76489,
|
||||
SN76494,
|
||||
SMS
|
||||
};
|
||||
|
||||
/// Creates a new SN76489.
|
||||
SN76489(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue, int additional_divider = 1);
|
||||
/// Creates a new SN76489.
|
||||
SN76489(Personality, Concurrency::AsyncTaskQueue<false> &, int additional_divider = 1);
|
||||
|
||||
/// Writes a new value to the SN76489.
|
||||
void write(uint8_t value);
|
||||
/// Writes a new value to the SN76489.
|
||||
void write(uint8_t);
|
||||
|
||||
// As per SampleSource.
|
||||
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);
|
||||
// As per SampleSource.
|
||||
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:
|
||||
int master_divider_ = 0;
|
||||
int master_divider_period_ = 16;
|
||||
int16_t output_volume_ = 0;
|
||||
void evaluate_output_volume();
|
||||
int volumes_[16];
|
||||
private:
|
||||
int master_divider_ = 0;
|
||||
int master_divider_period_ = 16;
|
||||
int16_t output_volume_ = 0;
|
||||
void evaluate_output_volume();
|
||||
int volumes_[16];
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
|
||||
struct ToneChannel {
|
||||
// Programmatically-set state; updated by the processor.
|
||||
uint16_t divider = 0;
|
||||
uint8_t volume = 0xf;
|
||||
struct ToneChannel {
|
||||
// Programmatically-set state; updated by the processor.
|
||||
uint16_t divider = 0;
|
||||
uint8_t volume = 0xf;
|
||||
|
||||
// Active state; self-evolving as a function of time.
|
||||
uint16_t counter = 0;
|
||||
int level = 0;
|
||||
} channels_[4];
|
||||
enum {
|
||||
Periodic15,
|
||||
Periodic16,
|
||||
Noise15,
|
||||
Noise16
|
||||
} noise_mode_ = Periodic15;
|
||||
uint16_t noise_shifter_ = 0;
|
||||
int active_register_ = 0;
|
||||
// Active state; self-evolving as a function of time.
|
||||
uint16_t counter = 0;
|
||||
int level = 0;
|
||||
} channels_[4];
|
||||
enum {
|
||||
Periodic15,
|
||||
Periodic16,
|
||||
Noise15,
|
||||
Noise16
|
||||
} noise_mode_ = Periodic15;
|
||||
uint16_t noise_shifter_ = 0;
|
||||
int active_register_ = 0;
|
||||
|
||||
bool shifter_is_16bit_ = false;
|
||||
bool shifter_is_16bit_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
using namespace Serial;
|
||||
|
||||
template <bool include_clock>
|
||||
void Line<include_clock>::set_writer_clock_rate(HalfCycles clock_rate) {
|
||||
void Line<include_clock>::set_writer_clock_rate(const HalfCycles clock_rate) {
|
||||
clock_rate_ = clock_rate;
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ void Line<include_clock>::advance_writer(HalfCycles cycles) {
|
||||
}
|
||||
|
||||
template <bool include_clock>
|
||||
void Line<include_clock>::write(bool level) {
|
||||
void Line<include_clock>::write(const bool level) {
|
||||
if(!events_.empty()) {
|
||||
events_.emplace_back();
|
||||
events_.back().type = level ? Event::SetHigh : Event::SetLow;
|
||||
@ -84,7 +84,11 @@ void Line<include_clock>::write(bool level) {
|
||||
}
|
||||
|
||||
template <bool include_clock>
|
||||
template <bool lsb_first, typename IntT> void Line<include_clock>::write_internal(HalfCycles cycles, int count, IntT levels) {
|
||||
template <bool lsb_first, typename IntT> void Line<include_clock>::write_internal(
|
||||
const HalfCycles cycles,
|
||||
int count,
|
||||
const IntT levels
|
||||
) {
|
||||
remaining_delays_ += count * cycles.as_integral();
|
||||
|
||||
auto event = events_.size();
|
||||
@ -108,12 +112,12 @@ template <bool lsb_first, typename IntT> void Line<include_clock>::write_interna
|
||||
}
|
||||
|
||||
template <bool include_clock>
|
||||
void Line<include_clock>::write(HalfCycles cycles, int count, int levels) {
|
||||
void Line<include_clock>::write(const HalfCycles cycles, const int count, const int levels) {
|
||||
write_internal<true, int>(cycles, count, levels);
|
||||
}
|
||||
|
||||
template <bool include_clock>
|
||||
template <bool lsb_first, typename IntT> void Line<include_clock>::write(HalfCycles cycles, IntT value) {
|
||||
template <bool lsb_first, typename IntT> void Line<include_clock>::write(const HalfCycles cycles, const IntT value) {
|
||||
write_internal<lsb_first, IntT>(cycles, 8 * sizeof(IntT), value);
|
||||
}
|
||||
|
||||
@ -129,7 +133,10 @@ bool Line<include_clock>::read() const {
|
||||
}
|
||||
|
||||
template <bool include_clock>
|
||||
void Line<include_clock>::set_read_delegate(ReadDelegate *delegate, [[maybe_unused]] Storage::Time bit_length) {
|
||||
void Line<include_clock>::set_read_delegate(
|
||||
ReadDelegate *const delegate,
|
||||
[[maybe_unused]] const Storage::Time bit_length
|
||||
) {
|
||||
read_delegate_ = delegate;
|
||||
if constexpr (!include_clock) {
|
||||
assert(bit_length > Storage::Time(0));
|
||||
@ -140,7 +147,7 @@ void Line<include_clock>::set_read_delegate(ReadDelegate *delegate, [[maybe_unus
|
||||
}
|
||||
|
||||
template <bool include_clock>
|
||||
void Line<include_clock>::update_delegate(bool level) {
|
||||
void Line<include_clock>::update_delegate(const bool level) {
|
||||
// Exit early if there's no delegate, or if the delegate is waiting for
|
||||
// zero and this isn't zero.
|
||||
if(!read_delegate_) return;
|
||||
|
@ -41,97 +41,96 @@ namespace Serial {
|
||||
all enqueued bits at appropriate times.
|
||||
*/
|
||||
template <bool include_clock> class Line {
|
||||
public:
|
||||
/// Sets the line to @c level instantaneously.
|
||||
void write(bool level);
|
||||
public:
|
||||
/// Sets the line to @c level instantaneously.
|
||||
void write(bool);
|
||||
|
||||
/// @returns The instantaneous level of this line.
|
||||
bool read() const;
|
||||
/// @returns The instantaneous level of this line.
|
||||
bool read() const;
|
||||
|
||||
/// Sets the denominator for the between levels for any data enqueued
|
||||
/// via an @c write.
|
||||
void set_writer_clock_rate(HalfCycles clock_rate);
|
||||
/// Sets the denominator for the between levels for any data enqueued
|
||||
/// via an @c write.
|
||||
void set_writer_clock_rate(HalfCycles);
|
||||
|
||||
/// Enqueues @c count level changes, the first occurring immediately
|
||||
/// after the final event currently posted and each subsequent event
|
||||
/// occurring @c cycles after the previous. An additional gap of @c cycles
|
||||
/// is scheduled after the final output. The levels to output are
|
||||
/// taken from @c levels which is read from lsb to msb. @c cycles is
|
||||
/// relative to the writer's clock rate.
|
||||
void write(HalfCycles cycles, int count, int levels);
|
||||
/// Enqueues @c count level changes, the first occurring immediately
|
||||
/// after the final event currently posted and each subsequent event
|
||||
/// occurring @c cycles after the previous. An additional gap of @c cycles
|
||||
/// is scheduled after the final output. The levels to output are
|
||||
/// taken from @c levels which is read from lsb to msb. @c cycles is
|
||||
/// relative to the writer's clock rate.
|
||||
void write(HalfCycles cycles, int count, int levels);
|
||||
|
||||
/// Enqueus every bit from @c value as per the rules of write(HalfCycles, int, int),
|
||||
/// either in LSB or MSB order as per the @c lsb_first template flag.
|
||||
template <bool lsb_first, typename IntT> void write(HalfCycles cycles, IntT value);
|
||||
/// Enqueus every bit from @c value as per the rules of write(HalfCycles, int, int),
|
||||
/// either in LSB or MSB order as per the @c lsb_first template flag.
|
||||
template <bool lsb_first, typename IntT> void write(HalfCycles cycles, IntT value);
|
||||
|
||||
/// @returns the number of cycles until currently enqueued write data is exhausted.
|
||||
forceinline HalfCycles write_data_time_remaining() const {
|
||||
return HalfCycles(remaining_delays_);
|
||||
}
|
||||
/// @returns the number of cycles until currently enqueued write data is exhausted.
|
||||
forceinline HalfCycles write_data_time_remaining() const {
|
||||
return HalfCycles(remaining_delays_);
|
||||
}
|
||||
|
||||
/// @returns the number of cycles left until it is guaranteed that a passive reader
|
||||
/// has received all currently-enqueued bits.
|
||||
forceinline HalfCycles transmission_data_time_remaining() const {
|
||||
return HalfCycles(remaining_delays_ + transmission_extra_);
|
||||
}
|
||||
/// @returns the number of cycles left until it is guaranteed that a passive reader
|
||||
/// has received all currently-enqueued bits.
|
||||
forceinline HalfCycles transmission_data_time_remaining() const {
|
||||
return HalfCycles(remaining_delays_ + transmission_extra_);
|
||||
}
|
||||
|
||||
/// Advances the read position by @c cycles relative to the writer's
|
||||
/// clock rate.
|
||||
void advance_writer(HalfCycles cycles);
|
||||
/// Advances the read position relative to the writer's clock rate.
|
||||
void advance_writer(HalfCycles);
|
||||
|
||||
/// Eliminates all future write states, leaving the output at whatever it is now.
|
||||
void reset_writing();
|
||||
/// Eliminates all future write states, leaving the output at whatever it is now.
|
||||
void reset_writing();
|
||||
|
||||
struct ReadDelegate {
|
||||
virtual bool serial_line_did_produce_bit(Line *line, int bit) = 0;
|
||||
};
|
||||
/*!
|
||||
Sets a read delegate.
|
||||
struct ReadDelegate {
|
||||
virtual bool serial_line_did_produce_bit(Line *, int bit) = 0;
|
||||
};
|
||||
/*!
|
||||
Sets a read delegate.
|
||||
|
||||
Single line serial connection:
|
||||
Single line serial connection:
|
||||
|
||||
The delegate will receive samples of the output level every
|
||||
@c bit_lengths of a second apart subject to a state machine:
|
||||
The delegate will receive samples of the output level every
|
||||
@c bit_lengths of a second apart subject to a state machine:
|
||||
|
||||
* initially no bits will be delivered;
|
||||
* when a zero level is first detected, the line will wait half a bit's length, then start
|
||||
sampling at single-bit intervals, passing each bit to the delegate while it returns @c true;
|
||||
* as soon as the delegate returns @c false, the line will return to the initial state.
|
||||
* initially no bits will be delivered;
|
||||
* when a zero level is first detected, the line will wait half a bit's length, then start
|
||||
sampling at single-bit intervals, passing each bit to the delegate while it returns @c true;
|
||||
* as soon as the delegate returns @c false, the line will return to the initial state.
|
||||
|
||||
Two-line clock + data connection:
|
||||
Two-line clock + data connection:
|
||||
|
||||
The delegate will receive every bit that has been enqueued, spaced as nominated
|
||||
by the writer. @c bit_length is ignored, as is the return result of
|
||||
@c ReadDelegate::serial_line_did_produce_bit.
|
||||
*/
|
||||
void set_read_delegate(ReadDelegate *delegate, Storage::Time bit_length = Storage::Time());
|
||||
The delegate will receive every bit that has been enqueued, spaced as nominated
|
||||
by the writer. @c bit_length is ignored, as is the return result of
|
||||
@c ReadDelegate::serial_line_did_produce_bit.
|
||||
*/
|
||||
void set_read_delegate(ReadDelegate *, Storage::Time bit_length = Storage::Time());
|
||||
|
||||
private:
|
||||
struct Event {
|
||||
enum Type {
|
||||
Delay, SetHigh, SetLow
|
||||
} type;
|
||||
int delay;
|
||||
};
|
||||
std::vector<Event> events_;
|
||||
HalfCycles::IntType remaining_delays_ = 0;
|
||||
HalfCycles::IntType transmission_extra_ = 0;
|
||||
bool level_ = true;
|
||||
HalfCycles clock_rate_ = 0;
|
||||
private:
|
||||
struct Event {
|
||||
enum Type {
|
||||
Delay, SetHigh, SetLow
|
||||
} type;
|
||||
int delay;
|
||||
};
|
||||
std::vector<Event> events_;
|
||||
HalfCycles::IntType remaining_delays_ = 0;
|
||||
HalfCycles::IntType transmission_extra_ = 0;
|
||||
bool level_ = true;
|
||||
HalfCycles clock_rate_ = 0;
|
||||
|
||||
ReadDelegate *read_delegate_ = nullptr;
|
||||
Storage::Time read_delegate_bit_length_, time_left_in_bit_;
|
||||
int write_cycles_since_delegate_call_ = 0;
|
||||
enum class ReadDelegatePhase {
|
||||
WaitingForZero,
|
||||
Serialising
|
||||
} read_delegate_phase_ = ReadDelegatePhase::WaitingForZero;
|
||||
ReadDelegate *read_delegate_ = nullptr;
|
||||
Storage::Time read_delegate_bit_length_, time_left_in_bit_;
|
||||
int write_cycles_since_delegate_call_ = 0;
|
||||
enum class ReadDelegatePhase {
|
||||
WaitingForZero,
|
||||
Serialising
|
||||
} read_delegate_phase_ = ReadDelegatePhase::WaitingForZero;
|
||||
|
||||
void update_delegate(bool level);
|
||||
HalfCycles::IntType minimum_write_cycles_for_read_delegate_bit();
|
||||
void update_delegate(bool level);
|
||||
HalfCycles::IntType minimum_write_cycles_for_read_delegate_bit();
|
||||
|
||||
template <bool lsb_first, typename IntT> void
|
||||
write_internal(HalfCycles, int, IntT);
|
||||
template <bool lsb_first, typename IntT> void
|
||||
write_internal(HalfCycles, int, IntT);
|
||||
};
|
||||
|
||||
/*!
|
||||
|
Loading…
x
Reference in New Issue
Block a user