mirror of
https://github.com/TomHarte/CLK.git
synced 2025-01-14 13:33:42 +00:00
Merge pull request #44 from TomHarte/6560Template
Shifted the 6560 into an idiomatic template form; switched automatic loading to optional.
This commit is contained in:
commit
0a13dd5c5b
@ -10,377 +10,19 @@
|
||||
|
||||
using namespace MOS;
|
||||
|
||||
MOS6560::MOS6560() :
|
||||
_crt(new Outputs::CRT::CRT(65*4, 4, Outputs::CRT::NTSC60, 1)),
|
||||
_speaker(new Speaker),
|
||||
_horizontal_counter(0),
|
||||
_vertical_counter(0),
|
||||
_cycles_since_speaker_update(0),
|
||||
_is_odd_frame(false)
|
||||
{
|
||||
_crt->set_composite_sampling_function(
|
||||
"float composite_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
|
||||
"{"
|
||||
"uint c = texture(texID, coordinate).r;"
|
||||
"float y = float(c >> 4) / 4.0;"
|
||||
"uint yC = c & 15u;"
|
||||
"float phaseOffset = 6.283185308 * float(yC) / 16.0;"
|
||||
|
||||
"float chroma = cos(phase + phaseOffset);"
|
||||
"return mix(y, step(yC, 14) * chroma, amplitude);"
|
||||
"}");
|
||||
|
||||
// default to NTSC
|
||||
set_output_mode(OutputMode::NTSC);
|
||||
|
||||
// show only the centre
|
||||
_crt->set_visible_area(_crt->get_rect_for_area(16, 237, 11*4, 55*4, 4.0f / 3.0f));
|
||||
_speaker->set_input_rate(255681.75); // assuming NTSC; clock rate / 4
|
||||
}
|
||||
|
||||
void MOS6560::set_output_mode(OutputMode output_mode)
|
||||
{
|
||||
uint8_t luminances[16] = { // range is 0–4
|
||||
0, 4, 1, 3, 2, 2, 1, 3,
|
||||
2, 1, 2, 1, 2, 3, 2, 3
|
||||
};
|
||||
uint8_t pal_chrominances[16] = { // range is 0–15; 15 is a special case meaning "no chrominance"
|
||||
15, 15, 5, 13, 2, 10, 0, 8,
|
||||
6, 7, 5, 13, 2, 10, 0, 8,
|
||||
};
|
||||
uint8_t ntsc_chrominances[16] = {
|
||||
15, 15, 2, 10, 4, 12, 6, 14,
|
||||
0, 8, 2, 10, 4, 12, 6, 14,
|
||||
};
|
||||
uint8_t *chrominances;
|
||||
Outputs::CRT::DisplayType display_type;
|
||||
|
||||
switch(output_mode)
|
||||
{
|
||||
case OutputMode::PAL:
|
||||
chrominances = pal_chrominances;
|
||||
display_type = Outputs::CRT::PAL50;
|
||||
_timing.cycles_per_line = 71;
|
||||
_timing.line_counter_increment_offset = 0;
|
||||
_timing.lines_per_progressive_field = 312;
|
||||
_timing.supports_interlacing = false;
|
||||
break;
|
||||
|
||||
case OutputMode::NTSC:
|
||||
chrominances = ntsc_chrominances;
|
||||
display_type = Outputs::CRT::NTSC60;
|
||||
_timing.cycles_per_line = 65;
|
||||
_timing.line_counter_increment_offset = 65 - 33; // TODO: this is a bit of a hack; separate vertical and horizontal counting
|
||||
_timing.lines_per_progressive_field = 261;
|
||||
_timing.supports_interlacing = true;
|
||||
break;
|
||||
}
|
||||
|
||||
_crt->set_new_display_type((unsigned int)(_timing.cycles_per_line*4), display_type);
|
||||
for(int c = 0; c < 16; c++)
|
||||
{
|
||||
_colours[c] = (uint8_t)((luminances[c] << 4) | chrominances[c]);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: set clock rate
|
||||
|
||||
void MOS6560::set_register(int address, uint8_t value)
|
||||
{
|
||||
address &= 0xf;
|
||||
_registers.direct_values[address] = value;
|
||||
switch(address)
|
||||
{
|
||||
case 0x0:
|
||||
_registers.interlaced = !!(value&0x80) && _timing.supports_interlacing;
|
||||
_registers.first_column_location = value & 0x7f;
|
||||
break;
|
||||
|
||||
case 0x1:
|
||||
_registers.first_row_location = value;
|
||||
break;
|
||||
|
||||
case 0x2:
|
||||
_registers.number_of_columns = value & 0x7f;
|
||||
_registers.video_matrix_start_address = (uint16_t)((_registers.video_matrix_start_address & 0x3c00) | ((value & 0x80) << 2));
|
||||
break;
|
||||
|
||||
case 0x3:
|
||||
_registers.number_of_rows = (value >> 1)&0x3f;
|
||||
_registers.tall_characters = !!(value&0x01);
|
||||
break;
|
||||
|
||||
case 0x5:
|
||||
_registers.character_cell_start_address = (uint16_t)((value & 0x0f) << 10);
|
||||
_registers.video_matrix_start_address = (uint16_t)((_registers.video_matrix_start_address & 0x0200) | ((value & 0xf0) << 6));
|
||||
break;
|
||||
|
||||
case 0xa:
|
||||
case 0xb:
|
||||
case 0xc:
|
||||
case 0xd:
|
||||
update_audio();
|
||||
_speaker->set_control(address - 0xa, value);
|
||||
break;
|
||||
|
||||
case 0xe:
|
||||
update_audio();
|
||||
_registers.auxiliary_colour = _colours[value >> 4];
|
||||
_speaker->set_volume(value & 0xf);
|
||||
break;
|
||||
|
||||
case 0xf:
|
||||
{
|
||||
uint8_t new_border_colour = _colours[value & 0x07];
|
||||
if(_this_state == State::Border && new_border_colour != _registers.borderColour)
|
||||
{
|
||||
output_border(_cycles_in_state * 4);
|
||||
_cycles_in_state = 0;
|
||||
}
|
||||
_registers.invertedCells = !((value >> 3)&1);
|
||||
_registers.borderColour = new_border_colour;
|
||||
_registers.backgroundColour = _colours[value >> 4];
|
||||
}
|
||||
break;
|
||||
|
||||
// TODO: the lightpen, etc
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t MOS6560::get_register(int address)
|
||||
{
|
||||
address &= 0xf;
|
||||
int current_line = (_full_frame_counter + _timing.line_counter_increment_offset) / _timing.cycles_per_line;
|
||||
switch(address)
|
||||
{
|
||||
default: return _registers.direct_values[address];
|
||||
case 0x03: return (uint8_t)(current_line << 7) | (_registers.direct_values[3] & 0x7f);
|
||||
case 0x04: return (current_line >> 1) & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
void MOS6560::output_border(unsigned int number_of_cycles)
|
||||
{
|
||||
uint8_t *colour_pointer = _crt->allocate_write_area(1);
|
||||
if(colour_pointer) *colour_pointer = _registers.borderColour;
|
||||
_crt->output_level(number_of_cycles);
|
||||
// _crt->output_blank(number_of_cycles);
|
||||
}
|
||||
|
||||
uint16_t MOS6560::get_address()
|
||||
{
|
||||
// keep track of the amount of time since the speaker was updated; lazy updates are applied
|
||||
_cycles_since_speaker_update++;
|
||||
|
||||
// keep an old copy of the vertical count because that test is a cycle later than the actual changes
|
||||
int previous_vertical_counter = _vertical_counter;
|
||||
|
||||
// keep track of internal time relative to this scanline
|
||||
_horizontal_counter++;
|
||||
_full_frame_counter++;
|
||||
if(_horizontal_counter == _timing.cycles_per_line)
|
||||
{
|
||||
if(_horizontal_drawing_latch)
|
||||
{
|
||||
_current_character_row++;
|
||||
if(
|
||||
(_current_character_row == 16) ||
|
||||
(_current_character_row == 8 && !_registers.tall_characters)
|
||||
) {
|
||||
_current_character_row = 0;
|
||||
_current_row++;
|
||||
}
|
||||
|
||||
_pixel_line_cycle = -1;
|
||||
_columns_this_line = -1;
|
||||
_column_counter = -1;
|
||||
}
|
||||
|
||||
_horizontal_counter = 0;
|
||||
_horizontal_drawing_latch = false;
|
||||
|
||||
_vertical_counter ++;
|
||||
if(_vertical_counter == (_registers.interlaced ? (_is_odd_frame ? 262 : 263) : _timing.lines_per_progressive_field))
|
||||
{
|
||||
_vertical_counter = 0;
|
||||
_full_frame_counter = 0;
|
||||
|
||||
_is_odd_frame ^= true;
|
||||
_current_row = 0;
|
||||
_rows_this_field = -1;
|
||||
_vertical_drawing_latch = false;
|
||||
_base_video_matrix_address_counter = 0;
|
||||
_current_character_row = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// check for vertical starting events
|
||||
_vertical_drawing_latch |= _registers.first_row_location == (previous_vertical_counter >> 1);
|
||||
_horizontal_drawing_latch |= _vertical_drawing_latch && (_horizontal_counter == _registers.first_column_location);
|
||||
|
||||
if(_pixel_line_cycle >= 0) _pixel_line_cycle++;
|
||||
switch(_pixel_line_cycle)
|
||||
{
|
||||
case -1:
|
||||
if(_horizontal_drawing_latch)
|
||||
{
|
||||
_pixel_line_cycle = 0;
|
||||
_video_matrix_address_counter = _base_video_matrix_address_counter;
|
||||
}
|
||||
break;
|
||||
case 1: _columns_this_line = _registers.number_of_columns; break;
|
||||
case 2: if(_rows_this_field < 0) _rows_this_field = _registers.number_of_rows; break;
|
||||
case 3: if(_current_row < _rows_this_field) _column_counter = 0; break;
|
||||
}
|
||||
|
||||
uint16_t fetch_address = 0x1c;
|
||||
if(_column_counter >= 0 && _column_counter < _columns_this_line*2)
|
||||
{
|
||||
if(_column_counter&1)
|
||||
{
|
||||
fetch_address = _registers.character_cell_start_address + (_character_code*(_registers.tall_characters ? 16 : 8)) + _current_character_row;
|
||||
}
|
||||
else
|
||||
{
|
||||
fetch_address = (uint16_t)(_registers.video_matrix_start_address + _video_matrix_address_counter);
|
||||
_video_matrix_address_counter++;
|
||||
if(
|
||||
(_current_character_row == 15) ||
|
||||
(_current_character_row == 7 && !_registers.tall_characters)
|
||||
) {
|
||||
_base_video_matrix_address_counter = _video_matrix_address_counter;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fetch_address & 0x3fff;
|
||||
}
|
||||
|
||||
void MOS6560::set_graphics_value(uint8_t value, uint8_t colour_value)
|
||||
{
|
||||
// TODO: there should be a further two-cycle delay on pixels being output; the reverse bit should
|
||||
// divide the byte it is set for 3:1 and then continue as usual.
|
||||
|
||||
// determine output state; colour burst and sync timing are currently a guess
|
||||
if(_horizontal_counter > _timing.cycles_per_line-4) _this_state = State::ColourBurst;
|
||||
else if(_horizontal_counter > _timing.cycles_per_line-7) _this_state = State::Sync;
|
||||
else
|
||||
{
|
||||
_this_state = (_column_counter >= 0 && _column_counter < _columns_this_line*2) ? State::Pixels : State::Border;
|
||||
}
|
||||
|
||||
// apply vertical sync
|
||||
if(
|
||||
(_vertical_counter < 3 && (_is_odd_frame || !_registers.interlaced)) ||
|
||||
(_registers.interlaced &&
|
||||
(
|
||||
(_vertical_counter == 0 && _horizontal_counter > 32) ||
|
||||
(_vertical_counter == 1) || (_vertical_counter == 2) ||
|
||||
(_vertical_counter == 3 && _horizontal_counter <= 32)
|
||||
)
|
||||
))
|
||||
_this_state = State::Sync;
|
||||
|
||||
// update the CRT
|
||||
if(_this_state != _output_state)
|
||||
{
|
||||
switch(_output_state)
|
||||
{
|
||||
case State::Sync: _crt->output_sync(_cycles_in_state * 4); break;
|
||||
case State::ColourBurst: _crt->output_colour_burst(_cycles_in_state * 4, _is_odd_frame ? 128 : 0, 0); break;
|
||||
case State::Border: output_border(_cycles_in_state * 4); break;
|
||||
case State::Pixels: _crt->output_data(_cycles_in_state * 4, 1); break;
|
||||
}
|
||||
_output_state = _this_state;
|
||||
_cycles_in_state = 0;
|
||||
|
||||
pixel_pointer = nullptr;
|
||||
if(_output_state == State::Pixels)
|
||||
{
|
||||
pixel_pointer = _crt->allocate_write_area(260);
|
||||
}
|
||||
}
|
||||
_cycles_in_state++;
|
||||
|
||||
if(_this_state == State::Pixels)
|
||||
{
|
||||
if(_column_counter&1)
|
||||
{
|
||||
_character_value = value;
|
||||
|
||||
if(pixel_pointer)
|
||||
{
|
||||
uint8_t cell_colour = _colours[_character_colour & 0x7];
|
||||
if(!(_character_colour&0x8))
|
||||
{
|
||||
uint8_t colours[2];
|
||||
if(_registers.invertedCells)
|
||||
{
|
||||
colours[0] = cell_colour;
|
||||
colours[1] = _registers.backgroundColour;
|
||||
}
|
||||
else
|
||||
{
|
||||
colours[0] = _registers.backgroundColour;
|
||||
colours[1] = cell_colour;
|
||||
}
|
||||
pixel_pointer[0] = colours[(_character_value >> 7)&1];
|
||||
pixel_pointer[1] = colours[(_character_value >> 6)&1];
|
||||
pixel_pointer[2] = colours[(_character_value >> 5)&1];
|
||||
pixel_pointer[3] = colours[(_character_value >> 4)&1];
|
||||
pixel_pointer[4] = colours[(_character_value >> 3)&1];
|
||||
pixel_pointer[5] = colours[(_character_value >> 2)&1];
|
||||
pixel_pointer[6] = colours[(_character_value >> 1)&1];
|
||||
pixel_pointer[7] = colours[(_character_value >> 0)&1];
|
||||
}
|
||||
else
|
||||
{
|
||||
uint8_t colours[4] = {_registers.backgroundColour, _registers.borderColour, cell_colour, _registers.auxiliary_colour};
|
||||
pixel_pointer[0] =
|
||||
pixel_pointer[1] = colours[(_character_value >> 6)&3];
|
||||
pixel_pointer[2] =
|
||||
pixel_pointer[3] = colours[(_character_value >> 4)&3];
|
||||
pixel_pointer[4] =
|
||||
pixel_pointer[5] = colours[(_character_value >> 2)&3];
|
||||
pixel_pointer[6] =
|
||||
pixel_pointer[7] = colours[(_character_value >> 0)&3];
|
||||
}
|
||||
pixel_pointer += 8;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_character_code = value;
|
||||
_character_colour = colour_value;
|
||||
}
|
||||
|
||||
_column_counter++;
|
||||
}
|
||||
}
|
||||
|
||||
void MOS6560::update_audio()
|
||||
{
|
||||
_speaker->run_for_cycles(_cycles_since_speaker_update >> 2);
|
||||
_cycles_since_speaker_update &= 3;
|
||||
}
|
||||
|
||||
#pragma mark - Audio
|
||||
|
||||
MOS6560::Speaker::Speaker() :
|
||||
Speaker::Speaker() :
|
||||
_volume(0),
|
||||
_control_registers{0, 0, 0, 0},
|
||||
_shift_registers{0, 0, 0, 0},
|
||||
_counters{2, 1, 0, 0} // create a slight phase offset for the three channels
|
||||
{}
|
||||
|
||||
void MOS6560::Speaker::set_volume(uint8_t volume)
|
||||
void Speaker::set_volume(uint8_t volume)
|
||||
{
|
||||
_volume = volume;
|
||||
}
|
||||
|
||||
void MOS6560::Speaker::set_control(int channel, uint8_t value)
|
||||
void Speaker::set_control(int channel, uint8_t value)
|
||||
{
|
||||
_control_registers[channel] = value;
|
||||
}
|
||||
@ -457,7 +99,7 @@ static uint8_t noise_pattern[] = {
|
||||
#define increment(r) _shift_registers[r] = (_shift_registers[r]+1)%8191;
|
||||
#define update(r, m, up) _counters[r]++; if((_counters[r] >> m) == 0x7f) { up(r); _counters[r] = _control_registers[r]&0x7f; }
|
||||
|
||||
void MOS6560::Speaker::get_samples(unsigned int number_of_samples, int16_t *target)
|
||||
void Speaker::get_samples(unsigned int number_of_samples, int16_t *target)
|
||||
{
|
||||
for(unsigned int c = 0; c < number_of_samples; c++)
|
||||
{
|
||||
@ -477,7 +119,7 @@ void MOS6560::Speaker::get_samples(unsigned int number_of_samples, int16_t *targ
|
||||
}
|
||||
}
|
||||
|
||||
void MOS6560::Speaker::skip_samples(unsigned int number_of_samples)
|
||||
void Speaker::skip_samples(unsigned int number_of_samples)
|
||||
{
|
||||
for(unsigned int c = 0; c < number_of_samples; c++)
|
||||
{
|
||||
|
@ -14,6 +14,24 @@
|
||||
|
||||
namespace MOS {
|
||||
|
||||
// audio state
|
||||
class Speaker: public ::Outputs::Filter<Speaker> {
|
||||
public:
|
||||
Speaker();
|
||||
|
||||
void set_volume(uint8_t volume);
|
||||
void set_control(int channel, uint8_t value);
|
||||
|
||||
void get_samples(unsigned int number_of_samples, int16_t *target);
|
||||
void skip_samples(unsigned int number_of_samples);
|
||||
|
||||
private:
|
||||
unsigned int _counters[4];
|
||||
unsigned int _shift_registers[4];
|
||||
uint8_t _control_registers[4];
|
||||
uint8_t _volume;
|
||||
};
|
||||
|
||||
/*!
|
||||
The 6560 Video Interface Chip ('VIC') is a video and audio output chip; it therefore vends both a @c CRT and a @c Speaker.
|
||||
|
||||
@ -22,30 +40,288 @@ namespace MOS {
|
||||
|
||||
@c set_register and @c get_register provide register access.
|
||||
*/
|
||||
class MOS6560 {
|
||||
template <class T> class MOS6560 {
|
||||
public:
|
||||
MOS6560();
|
||||
MOS6560() :
|
||||
_crt(new Outputs::CRT::CRT(65*4, 4, Outputs::CRT::NTSC60, 1)),
|
||||
_speaker(new Speaker),
|
||||
_horizontal_counter(0),
|
||||
_vertical_counter(0),
|
||||
_cycles_since_speaker_update(0),
|
||||
_is_odd_frame(false)
|
||||
{
|
||||
_crt->set_composite_sampling_function(
|
||||
"float composite_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
|
||||
"{"
|
||||
"uint c = texture(texID, coordinate).r;"
|
||||
"float y = float(c >> 4) / 4.0;"
|
||||
"uint yC = c & 15u;"
|
||||
"float phaseOffset = 6.283185308 * float(yC) / 16.0;"
|
||||
|
||||
"float chroma = cos(phase + phaseOffset);"
|
||||
"return mix(y, step(yC, 14) * chroma, amplitude);"
|
||||
"}");
|
||||
|
||||
// default to NTSC
|
||||
set_output_mode(OutputMode::NTSC);
|
||||
|
||||
// show only the centre
|
||||
_crt->set_visible_area(_crt->get_rect_for_area(16, 237, 11*4, 55*4, 4.0f / 3.0f));
|
||||
_speaker->set_input_rate(255681.75); // assuming NTSC; clock rate / 4
|
||||
}
|
||||
|
||||
std::shared_ptr<Outputs::CRT::CRT> get_crt() { return _crt; }
|
||||
std::shared_ptr<Outputs::Speaker> get_speaker() { return _speaker; }
|
||||
|
||||
enum OutputMode {
|
||||
PAL, NTSC
|
||||
};
|
||||
|
||||
/*!
|
||||
Sets the output mode to either PAL or NTSC.
|
||||
*/
|
||||
void set_output_mode(OutputMode output_mode);
|
||||
void set_output_mode(OutputMode output_mode)
|
||||
{
|
||||
uint8_t luminances[16] = { // range is 0–4
|
||||
0, 4, 1, 3, 2, 2, 1, 3,
|
||||
2, 1, 2, 1, 2, 3, 2, 3
|
||||
};
|
||||
uint8_t pal_chrominances[16] = { // range is 0–15; 15 is a special case meaning "no chrominance"
|
||||
15, 15, 5, 13, 2, 10, 0, 8,
|
||||
6, 7, 5, 13, 2, 10, 0, 8,
|
||||
};
|
||||
uint8_t ntsc_chrominances[16] = {
|
||||
15, 15, 2, 10, 4, 12, 6, 14,
|
||||
0, 8, 2, 10, 4, 12, 6, 14,
|
||||
};
|
||||
uint8_t *chrominances;
|
||||
Outputs::CRT::DisplayType display_type;
|
||||
|
||||
switch(output_mode)
|
||||
{
|
||||
case OutputMode::PAL:
|
||||
chrominances = pal_chrominances;
|
||||
display_type = Outputs::CRT::PAL50;
|
||||
_timing.cycles_per_line = 71;
|
||||
_timing.line_counter_increment_offset = 0;
|
||||
_timing.lines_per_progressive_field = 312;
|
||||
_timing.supports_interlacing = false;
|
||||
break;
|
||||
|
||||
case OutputMode::NTSC:
|
||||
chrominances = ntsc_chrominances;
|
||||
display_type = Outputs::CRT::NTSC60;
|
||||
_timing.cycles_per_line = 65;
|
||||
_timing.line_counter_increment_offset = 65 - 33; // TODO: this is a bit of a hack; separate vertical and horizontal counting
|
||||
_timing.lines_per_progressive_field = 261;
|
||||
_timing.supports_interlacing = true;
|
||||
break;
|
||||
}
|
||||
|
||||
_crt->set_new_display_type((unsigned int)(_timing.cycles_per_line*4), display_type);
|
||||
for(int c = 0; c < 16; c++)
|
||||
{
|
||||
_colours[c] = (uint8_t)((luminances[c] << 4) | chrominances[c]);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Impliedly runs the 6560 for a single cycle, returning the next address that it puts on the bus.
|
||||
Runs for cycles. Derr.
|
||||
*/
|
||||
uint16_t get_address();
|
||||
inline void run_for_cycles(unsigned int number_of_cycles)
|
||||
{
|
||||
// keep track of the amount of time since the speaker was updated; lazy updates are applied
|
||||
_cycles_since_speaker_update += number_of_cycles;
|
||||
|
||||
/*!
|
||||
An owning machine should determine the state of the data bus as a result of the access implied
|
||||
by @c get_address and supply it to set_graphics_value.
|
||||
*/
|
||||
void set_graphics_value(uint8_t value, uint8_t colour_value);
|
||||
while(number_of_cycles--)
|
||||
{
|
||||
// keep an old copy of the vertical count because that test is a cycle later than the actual changes
|
||||
int previous_vertical_counter = _vertical_counter;
|
||||
|
||||
// keep track of internal time relative to this scanline
|
||||
_horizontal_counter++;
|
||||
_full_frame_counter++;
|
||||
if(_horizontal_counter == _timing.cycles_per_line)
|
||||
{
|
||||
if(_horizontal_drawing_latch)
|
||||
{
|
||||
_current_character_row++;
|
||||
if(
|
||||
(_current_character_row == 16) ||
|
||||
(_current_character_row == 8 && !_registers.tall_characters)
|
||||
) {
|
||||
_current_character_row = 0;
|
||||
_current_row++;
|
||||
}
|
||||
|
||||
_pixel_line_cycle = -1;
|
||||
_columns_this_line = -1;
|
||||
_column_counter = -1;
|
||||
}
|
||||
|
||||
_horizontal_counter = 0;
|
||||
_horizontal_drawing_latch = false;
|
||||
|
||||
_vertical_counter ++;
|
||||
if(_vertical_counter == (_registers.interlaced ? (_is_odd_frame ? 262 : 263) : _timing.lines_per_progressive_field))
|
||||
{
|
||||
_vertical_counter = 0;
|
||||
_full_frame_counter = 0;
|
||||
|
||||
_is_odd_frame ^= true;
|
||||
_current_row = 0;
|
||||
_rows_this_field = -1;
|
||||
_vertical_drawing_latch = false;
|
||||
_base_video_matrix_address_counter = 0;
|
||||
_current_character_row = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// check for vertical starting events
|
||||
_vertical_drawing_latch |= _registers.first_row_location == (previous_vertical_counter >> 1);
|
||||
_horizontal_drawing_latch |= _vertical_drawing_latch && (_horizontal_counter == _registers.first_column_location);
|
||||
|
||||
if(_pixel_line_cycle >= 0) _pixel_line_cycle++;
|
||||
switch(_pixel_line_cycle)
|
||||
{
|
||||
case -1:
|
||||
if(_horizontal_drawing_latch)
|
||||
{
|
||||
_pixel_line_cycle = 0;
|
||||
_video_matrix_address_counter = _base_video_matrix_address_counter;
|
||||
}
|
||||
break;
|
||||
case 1: _columns_this_line = _registers.number_of_columns; break;
|
||||
case 2: if(_rows_this_field < 0) _rows_this_field = _registers.number_of_rows; break;
|
||||
case 3: if(_current_row < _rows_this_field) _column_counter = 0; break;
|
||||
}
|
||||
|
||||
uint16_t fetch_address = 0x1c;
|
||||
if(_column_counter >= 0 && _column_counter < _columns_this_line*2)
|
||||
{
|
||||
if(_column_counter&1)
|
||||
{
|
||||
fetch_address = _registers.character_cell_start_address + (_character_code*(_registers.tall_characters ? 16 : 8)) + _current_character_row;
|
||||
}
|
||||
else
|
||||
{
|
||||
fetch_address = (uint16_t)(_registers.video_matrix_start_address + _video_matrix_address_counter);
|
||||
_video_matrix_address_counter++;
|
||||
if(
|
||||
(_current_character_row == 15) ||
|
||||
(_current_character_row == 7 && !_registers.tall_characters)
|
||||
) {
|
||||
_base_video_matrix_address_counter = _video_matrix_address_counter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetch_address &= 0x3fff;
|
||||
|
||||
uint8_t pixel_data;
|
||||
uint8_t colour_data;
|
||||
static_cast<T *>(this)->perform_read(fetch_address, &pixel_data, &colour_data);
|
||||
|
||||
// TODO: there should be a further two-cycle delay on pixels being output; the reverse bit should
|
||||
// divide the byte it is set for 3:1 and then continue as usual.
|
||||
|
||||
// determine output state; colour burst and sync timing are currently a guess
|
||||
if(_horizontal_counter > _timing.cycles_per_line-4) _this_state = State::ColourBurst;
|
||||
else if(_horizontal_counter > _timing.cycles_per_line-7) _this_state = State::Sync;
|
||||
else
|
||||
{
|
||||
_this_state = (_column_counter >= 0 && _column_counter < _columns_this_line*2) ? State::Pixels : State::Border;
|
||||
}
|
||||
|
||||
// apply vertical sync
|
||||
if(
|
||||
(_vertical_counter < 3 && (_is_odd_frame || !_registers.interlaced)) ||
|
||||
(_registers.interlaced &&
|
||||
(
|
||||
(_vertical_counter == 0 && _horizontal_counter > 32) ||
|
||||
(_vertical_counter == 1) || (_vertical_counter == 2) ||
|
||||
(_vertical_counter == 3 && _horizontal_counter <= 32)
|
||||
)
|
||||
))
|
||||
_this_state = State::Sync;
|
||||
|
||||
// update the CRT
|
||||
if(_this_state != _output_state)
|
||||
{
|
||||
switch(_output_state)
|
||||
{
|
||||
case State::Sync: _crt->output_sync(_cycles_in_state * 4); break;
|
||||
case State::ColourBurst: _crt->output_colour_burst(_cycles_in_state * 4, _is_odd_frame ? 128 : 0, 0); break;
|
||||
case State::Border: output_border(_cycles_in_state * 4); break;
|
||||
case State::Pixels: _crt->output_data(_cycles_in_state * 4, 1); break;
|
||||
}
|
||||
_output_state = _this_state;
|
||||
_cycles_in_state = 0;
|
||||
|
||||
pixel_pointer = nullptr;
|
||||
if(_output_state == State::Pixels)
|
||||
{
|
||||
pixel_pointer = _crt->allocate_write_area(260);
|
||||
}
|
||||
}
|
||||
_cycles_in_state++;
|
||||
|
||||
if(_this_state == State::Pixels)
|
||||
{
|
||||
if(_column_counter&1)
|
||||
{
|
||||
_character_value = pixel_data;
|
||||
|
||||
if(pixel_pointer)
|
||||
{
|
||||
uint8_t cell_colour = _colours[_character_colour & 0x7];
|
||||
if(!(_character_colour&0x8))
|
||||
{
|
||||
uint8_t colours[2];
|
||||
if(_registers.invertedCells)
|
||||
{
|
||||
colours[0] = cell_colour;
|
||||
colours[1] = _registers.backgroundColour;
|
||||
}
|
||||
else
|
||||
{
|
||||
colours[0] = _registers.backgroundColour;
|
||||
colours[1] = cell_colour;
|
||||
}
|
||||
pixel_pointer[0] = colours[(_character_value >> 7)&1];
|
||||
pixel_pointer[1] = colours[(_character_value >> 6)&1];
|
||||
pixel_pointer[2] = colours[(_character_value >> 5)&1];
|
||||
pixel_pointer[3] = colours[(_character_value >> 4)&1];
|
||||
pixel_pointer[4] = colours[(_character_value >> 3)&1];
|
||||
pixel_pointer[5] = colours[(_character_value >> 2)&1];
|
||||
pixel_pointer[6] = colours[(_character_value >> 1)&1];
|
||||
pixel_pointer[7] = colours[(_character_value >> 0)&1];
|
||||
}
|
||||
else
|
||||
{
|
||||
uint8_t colours[4] = {_registers.backgroundColour, _registers.borderColour, cell_colour, _registers.auxiliary_colour};
|
||||
pixel_pointer[0] =
|
||||
pixel_pointer[1] = colours[(_character_value >> 6)&3];
|
||||
pixel_pointer[2] =
|
||||
pixel_pointer[3] = colours[(_character_value >> 4)&3];
|
||||
pixel_pointer[4] =
|
||||
pixel_pointer[5] = colours[(_character_value >> 2)&3];
|
||||
pixel_pointer[6] =
|
||||
pixel_pointer[7] = colours[(_character_value >> 0)&3];
|
||||
}
|
||||
pixel_pointer += 8;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_character_code = pixel_data;
|
||||
_character_colour = colour_data;
|
||||
}
|
||||
|
||||
_column_counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Causes the 6560 to flush as much pending CRT and speaker communications as possible.
|
||||
@ -55,36 +331,96 @@ class MOS6560 {
|
||||
/*!
|
||||
Writes to a 6560 register.
|
||||
*/
|
||||
void set_register(int address, uint8_t value);
|
||||
void set_register(int address, uint8_t value)
|
||||
{
|
||||
address &= 0xf;
|
||||
_registers.direct_values[address] = value;
|
||||
switch(address)
|
||||
{
|
||||
case 0x0:
|
||||
_registers.interlaced = !!(value&0x80) && _timing.supports_interlacing;
|
||||
_registers.first_column_location = value & 0x7f;
|
||||
break;
|
||||
|
||||
case 0x1:
|
||||
_registers.first_row_location = value;
|
||||
break;
|
||||
|
||||
case 0x2:
|
||||
_registers.number_of_columns = value & 0x7f;
|
||||
_registers.video_matrix_start_address = (uint16_t)((_registers.video_matrix_start_address & 0x3c00) | ((value & 0x80) << 2));
|
||||
break;
|
||||
|
||||
case 0x3:
|
||||
_registers.number_of_rows = (value >> 1)&0x3f;
|
||||
_registers.tall_characters = !!(value&0x01);
|
||||
break;
|
||||
|
||||
case 0x5:
|
||||
_registers.character_cell_start_address = (uint16_t)((value & 0x0f) << 10);
|
||||
_registers.video_matrix_start_address = (uint16_t)((_registers.video_matrix_start_address & 0x0200) | ((value & 0xf0) << 6));
|
||||
break;
|
||||
|
||||
case 0xa:
|
||||
case 0xb:
|
||||
case 0xc:
|
||||
case 0xd:
|
||||
update_audio();
|
||||
_speaker->set_control(address - 0xa, value);
|
||||
break;
|
||||
|
||||
case 0xe:
|
||||
update_audio();
|
||||
_registers.auxiliary_colour = _colours[value >> 4];
|
||||
_speaker->set_volume(value & 0xf);
|
||||
break;
|
||||
|
||||
case 0xf:
|
||||
{
|
||||
uint8_t new_border_colour = _colours[value & 0x07];
|
||||
if(_this_state == State::Border && new_border_colour != _registers.borderColour)
|
||||
{
|
||||
output_border(_cycles_in_state * 4);
|
||||
_cycles_in_state = 0;
|
||||
}
|
||||
_registers.invertedCells = !((value >> 3)&1);
|
||||
_registers.borderColour = new_border_colour;
|
||||
_registers.backgroundColour = _colours[value >> 4];
|
||||
}
|
||||
break;
|
||||
|
||||
// TODO: the lightpen, etc
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Reads from a 6560 register.
|
||||
*/
|
||||
uint8_t get_register(int address);
|
||||
uint8_t get_register(int address)
|
||||
{
|
||||
address &= 0xf;
|
||||
int current_line = (_full_frame_counter + _timing.line_counter_increment_offset) / _timing.cycles_per_line;
|
||||
switch(address)
|
||||
{
|
||||
default: return _registers.direct_values[address];
|
||||
case 0x03: return (uint8_t)(current_line << 7) | (_registers.direct_values[3] & 0x7f);
|
||||
case 0x04: return (current_line >> 1) & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<Outputs::CRT::CRT> _crt;
|
||||
|
||||
// audio state
|
||||
class Speaker: public ::Outputs::Filter<Speaker> {
|
||||
public:
|
||||
Speaker();
|
||||
|
||||
void set_volume(uint8_t volume);
|
||||
void set_control(int channel, uint8_t value);
|
||||
|
||||
void get_samples(unsigned int number_of_samples, int16_t *target);
|
||||
void skip_samples(unsigned int number_of_samples);
|
||||
|
||||
private:
|
||||
unsigned int _counters[4];
|
||||
unsigned int _shift_registers[4];
|
||||
uint8_t _control_registers[4];
|
||||
uint8_t _volume;
|
||||
};
|
||||
std::shared_ptr<Speaker> _speaker;
|
||||
unsigned int _cycles_since_speaker_update;
|
||||
void update_audio();
|
||||
void update_audio()
|
||||
{
|
||||
_speaker->run_for_cycles(_cycles_since_speaker_update >> 2);
|
||||
_cycles_since_speaker_update &= 3;
|
||||
}
|
||||
|
||||
// register state
|
||||
struct {
|
||||
@ -126,7 +462,12 @@ class MOS6560 {
|
||||
uint8_t _colours[16];
|
||||
|
||||
uint8_t *pixel_pointer;
|
||||
void output_border(unsigned int number_of_cycles);
|
||||
void output_border(unsigned int number_of_cycles)
|
||||
{
|
||||
uint8_t *colour_pointer = _crt->allocate_write_area(1);
|
||||
if(colour_pointer) *colour_pointer = _registers.borderColour;
|
||||
_crt->output_level(number_of_cycles);
|
||||
}
|
||||
|
||||
struct {
|
||||
int cycles_per_line;
|
||||
|
@ -35,14 +35,9 @@ Machine::Machine() :
|
||||
_tape.set_delegate(this);
|
||||
|
||||
// establish the memory maps
|
||||
memset(_videoMemoryMap, 0, sizeof(_videoMemoryMap));
|
||||
memset(_processorReadMemoryMap, 0, sizeof(_processorReadMemoryMap));
|
||||
memset(_processorWriteMemoryMap, 0, sizeof(_processorWriteMemoryMap));
|
||||
|
||||
write_to_map(_videoMemoryMap, _characterROM, 0x0000, sizeof(_characterROM));
|
||||
write_to_map(_videoMemoryMap, _userBASICMemory, 0x2000, sizeof(_userBASICMemory));
|
||||
write_to_map(_videoMemoryMap, _screenMemory, 0x3000, sizeof(_screenMemory));
|
||||
|
||||
write_to_map(_processorReadMemoryMap, _userBASICMemory, 0x0000, sizeof(_userBASICMemory));
|
||||
write_to_map(_processorReadMemoryMap, _screenMemory, 0x1000, sizeof(_screenMemory));
|
||||
write_to_map(_processorReadMemoryMap, _colorMemory, 0x9400, sizeof(_colorMemory));
|
||||
@ -86,9 +81,7 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
// }
|
||||
|
||||
// run the phase-1 part of this cycle, in which the VIC accesses memory
|
||||
uint16_t video_address = _mos6560->get_address();
|
||||
uint8_t video_value = _videoMemoryMap[video_address >> 10] ? _videoMemoryMap[video_address >> 10][video_address & 0x3ff] : 0xff; // TODO
|
||||
_mos6560->set_graphics_value(video_value, _colorMemory[video_address & 0x03ff]);
|
||||
_mos6560->run_for_cycles(1);
|
||||
|
||||
// run the phase-2 part of the cycle, which is whatever the 6502 said it should be
|
||||
if(isReadOperation(operation))
|
||||
@ -128,7 +121,11 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
|
||||
|
||||
_userPortVIA->run_for_half_cycles(2);
|
||||
_keyboardVIA->run_for_half_cycles(2);
|
||||
if(_typer) _typer->update(1);
|
||||
if(_typer && operation == CPU6502::BusOperation::ReadOpcode && address == 0xEB1E)
|
||||
{
|
||||
if(!_typer->type_next_character())
|
||||
_typer.reset();
|
||||
}
|
||||
_tape.run_for_cycles(1);
|
||||
if(_c1540) _c1540->run_for_cycles(1);
|
||||
return 1;
|
||||
@ -146,7 +143,13 @@ void Machine::mos6522_did_change_interrupt_status(void *mos6522)
|
||||
|
||||
void Machine::setup_output(float aspect_ratio)
|
||||
{
|
||||
_mos6560.reset(new MOS::MOS6560());
|
||||
_mos6560.reset(new Vic6560());
|
||||
|
||||
memset(_mos6560->_videoMemoryMap, 0, sizeof(_mos6560->_videoMemoryMap));
|
||||
write_to_map(_mos6560->_videoMemoryMap, _characterROM, 0x0000, sizeof(_characterROM));
|
||||
write_to_map(_mos6560->_videoMemoryMap, _userBASICMemory, 0x2000, sizeof(_userBASICMemory));
|
||||
write_to_map(_mos6560->_videoMemoryMap, _screenMemory, 0x3000, sizeof(_screenMemory));
|
||||
_mos6560->_colorMemory = _colorMemory;
|
||||
}
|
||||
|
||||
void Machine::close_output()
|
||||
@ -164,14 +167,9 @@ void Machine::set_rom(ROMSlot slot, size_t length, const uint8_t *data)
|
||||
case Characters: target = _characterROM; max_length = 0x1000; break;
|
||||
case BASIC: target = _basicROM; break;
|
||||
case Drive:
|
||||
if(_c1540)
|
||||
_c1540->set_rom(data);
|
||||
else
|
||||
{
|
||||
// if there's no 1540 now, hold onto the ROM in case one is added later
|
||||
_driveROM.reset(new uint8_t[length]);
|
||||
memcpy(_driveROM.get(), data, length);
|
||||
}
|
||||
_driveROM.reset(new uint8_t[length]);
|
||||
memcpy(_driveROM.get(), data, length);
|
||||
install_disk_rom();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -188,7 +186,7 @@ void Machine::add_prg(size_t length, const uint8_t *data)
|
||||
{
|
||||
_rom_address = (uint16_t)(data[0] | (data[1] << 8));
|
||||
_rom_length = (uint16_t)(length - 2);
|
||||
if(_rom_address >= 0x1000 && _rom_address+_rom_length < 0x2000)
|
||||
if(_rom_address >= 0x1000 && _rom_address+_rom_length < 0x2000 && _should_automatically_load_media)
|
||||
{
|
||||
set_typer_for_string("RUN\n");
|
||||
}
|
||||
@ -204,7 +202,7 @@ void Machine::add_prg(size_t length, const uint8_t *data)
|
||||
void Machine::set_tape(std::shared_ptr<Storage::Tape> tape)
|
||||
{
|
||||
_tape.set_tape(tape);
|
||||
set_typer_for_string("LOAD\n");
|
||||
if(_should_automatically_load_media) set_typer_for_string("LOAD\nRUN\n");
|
||||
}
|
||||
|
||||
void Machine::tape_did_change_input(Tape *tape)
|
||||
@ -226,13 +224,19 @@ void Machine::set_disk(std::shared_ptr<Storage::Disk> disk)
|
||||
_c1540->set_disk(disk);
|
||||
|
||||
// install the ROM if it was previously set
|
||||
if(_driveROM)
|
||||
install_disk_rom();
|
||||
|
||||
if(_should_automatically_load_media) set_typer_for_string("LOAD\"*\",8,1\nRUN\n");
|
||||
}
|
||||
|
||||
void Machine::install_disk_rom()
|
||||
{
|
||||
if(_driveROM && _c1540)
|
||||
{
|
||||
_c1540->set_rom(_driveROM.get());
|
||||
_c1540->run_for_cycles(2000000);
|
||||
_driveROM.reset();
|
||||
}
|
||||
|
||||
set_typer_for_string("LOAD\"*\",8,1\n");
|
||||
}
|
||||
|
||||
#pragma mark - Typer
|
||||
|
@ -225,6 +225,17 @@ class Tape: public Storage::TapePlayer {
|
||||
bool _input_level;
|
||||
};
|
||||
|
||||
class Vic6560: public MOS::MOS6560<Vic6560> {
|
||||
public:
|
||||
inline void perform_read(uint16_t address, uint8_t *pixel_data, uint8_t *colour_data)
|
||||
{
|
||||
*pixel_data = _videoMemoryMap[address >> 10] ? _videoMemoryMap[address >> 10][address & 0x3ff] : 0xff; // TODO
|
||||
*colour_data = _colorMemory[address & 0x03ff];
|
||||
}
|
||||
|
||||
uint8_t *_videoMemoryMap[16];
|
||||
uint8_t *_colorMemory;
|
||||
};
|
||||
|
||||
class Machine:
|
||||
public CPU6502::Processor<Machine>,
|
||||
@ -250,6 +261,7 @@ class Machine:
|
||||
}
|
||||
|
||||
inline void set_use_fast_tape_hack(bool activate) { _use_fast_tape_hack = activate; }
|
||||
inline void set_should_automatically_load_media(bool activate) { _should_automatically_load_media = activate; }
|
||||
|
||||
// to satisfy CPU6502::Processor
|
||||
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
|
||||
@ -289,12 +301,11 @@ class Machine:
|
||||
uint8_t _junkMemory[0x0400];
|
||||
std::unique_ptr<uint8_t> _driveROM;
|
||||
|
||||
uint8_t *_videoMemoryMap[16];
|
||||
uint8_t *_processorReadMemoryMap[64];
|
||||
uint8_t *_processorWriteMemoryMap[64];
|
||||
void write_to_map(uint8_t **map, uint8_t *area, uint16_t address, uint16_t length);
|
||||
|
||||
std::unique_ptr<MOS::MOS6560> _mos6560;
|
||||
std::unique_ptr<Vic6560> _mos6560;
|
||||
std::shared_ptr<UserPortVIA> _userPortVIA;
|
||||
std::shared_ptr<KeyboardVIA> _keyboardVIA;
|
||||
std::shared_ptr<SerialPort> _serialPort;
|
||||
@ -303,10 +314,13 @@ class Machine:
|
||||
|
||||
// Tape
|
||||
Tape _tape;
|
||||
bool _use_fast_tape_hack;
|
||||
bool _use_fast_tape_hack, _should_automatically_load_media;
|
||||
|
||||
// Disc
|
||||
// Disk
|
||||
std::shared_ptr<::Commodore::C1540::Machine> _c1540;
|
||||
void install_disk_rom();
|
||||
|
||||
// Autoload string
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -32,8 +32,10 @@ void Typer::update(int duration)
|
||||
}
|
||||
}
|
||||
|
||||
void Typer::type_next_character()
|
||||
bool Typer::type_next_character()
|
||||
{
|
||||
if(_string == nullptr) return false;
|
||||
|
||||
if(_delegate->typer_set_next_character(this, _string[_string_pointer], _phase))
|
||||
{
|
||||
_phase = 0;
|
||||
@ -41,7 +43,7 @@ void Typer::type_next_character()
|
||||
{
|
||||
free(_string);
|
||||
_string = nullptr;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
_string_pointer++;
|
||||
@ -50,6 +52,8 @@ void Typer::type_next_character()
|
||||
{
|
||||
_phase++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Typer::~Typer()
|
||||
|
@ -23,6 +23,7 @@ class Typer {
|
||||
Typer(const char *string, int delay, int frequency, Delegate *delegate);
|
||||
~Typer();
|
||||
void update(int duration);
|
||||
bool type_next_character();
|
||||
|
||||
private:
|
||||
char *_string;
|
||||
@ -31,8 +32,6 @@ class Typer {
|
||||
int _phase;
|
||||
Delegate *_delegate;
|
||||
size_t _string_pointer;
|
||||
|
||||
void type_next_character();
|
||||
};
|
||||
|
||||
class TypeRecipient: public Typer::Delegate {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9532" systemVersion="15F34" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9532" systemVersion="15G31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9532"/>
|
||||
</dependencies>
|
||||
@ -7,6 +7,7 @@
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="Vic20Document" customModule="Clock_Signal" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="fastLoadingButton" destination="sBT-cU-h7s" id="gWf-9E-D7l"/>
|
||||
<outlet property="loadAutomaticallyButton" destination="lbt-Wo-6fc" id="Xsc-dz-1a6"/>
|
||||
<outlet property="openGLView" destination="DEG-fq-cjd" id="Gxs-2u-n7B"/>
|
||||
<outlet property="optionsPanel" destination="ota-g7-hOL" id="zeO-di-9i3"/>
|
||||
<outlet property="window" destination="xOd-HO-29H" id="JIz-fz-R2o"/>
|
||||
@ -43,17 +44,17 @@
|
||||
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ota-g7-hOL" customClass="NSPanel">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" HUD="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="83" y="102" width="200" height="83"/>
|
||||
<rect key="contentRect" x="83" y="102" width="200" height="103"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1366" height="768"/>
|
||||
<value key="minSize" type="size" width="200" height="83"/>
|
||||
<value key="maxSize" type="size" width="200" height="83"/>
|
||||
<value key="minSize" type="size" width="200" height="103"/>
|
||||
<value key="maxSize" type="size" width="200" height="103"/>
|
||||
<view key="contentView" id="7Pv-WL-2Rq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="83"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="200" height="103"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="sBT-cU-h7s">
|
||||
<rect key="frame" x="18" y="47" width="164" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Load Quickly" bezelStyle="regularSquare" imagePosition="left" alignment="left" state="on" inset="2" id="w0l-ha-esm">
|
||||
<rect key="frame" x="18" y="67" width="164" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Load Tapes Quickly" bezelStyle="regularSquare" imagePosition="left" alignment="left" state="on" inset="2" id="w0l-ha-esm">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
@ -61,6 +62,16 @@
|
||||
<action selector="setFastLoading:" target="-2" id="ctR-h1-CYI"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="lbt-Wo-6fc">
|
||||
<rect key="frame" x="18" y="47" width="164" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Load Automatically" bezelStyle="regularSquare" imagePosition="left" alignment="left" state="on" inset="2" id="jTj-uV-at1">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="setShouldLoadAutomatically:" target="-2" id="Ixe-HN-4XK"/>
|
||||
</connections>
|
||||
</button>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MlB-rE-TXV">
|
||||
<rect key="frame" x="18" y="17" width="165" height="26"/>
|
||||
<popUpButtonCell key="cell" type="push" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" id="UIu-uz-pTu">
|
||||
@ -72,7 +83,7 @@
|
||||
<menuItem title="European Machine" id="5ju-Z0-BDa"/>
|
||||
<menuItem title="Japanese Machine" id="YlT-9e-azY"/>
|
||||
<menuItem title="Swedish Machine" id="joU-Bt-XFb"/>
|
||||
<menuItem title="US Machine" state="on" id="FXe-ca-cTY"/>
|
||||
<menuItem title="US Machine" id="FXe-ca-cTY"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
@ -80,14 +91,17 @@
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="sBT-cU-h7s" secondAttribute="trailing" constant="20" id="79b-2A-2c7"/>
|
||||
<constraint firstItem="MlB-rE-TXV" firstAttribute="top" secondItem="lbt-Wo-6fc" secondAttribute="bottom" constant="8" id="DIc-Sm-VlA"/>
|
||||
<constraint firstItem="sBT-cU-h7s" firstAttribute="top" secondItem="7Pv-WL-2Rq" secondAttribute="top" constant="20" id="E5m-wo-X92"/>
|
||||
<constraint firstItem="MlB-rE-TXV" firstAttribute="top" secondItem="sBT-cU-h7s" secondAttribute="bottom" constant="8" id="fis-Fe-CkQ"/>
|
||||
<constraint firstItem="lbt-Wo-6fc" firstAttribute="leading" secondItem="7Pv-WL-2Rq" secondAttribute="leading" constant="20" id="cID-bi-rVP"/>
|
||||
<constraint firstItem="lbt-Wo-6fc" firstAttribute="top" secondItem="sBT-cU-h7s" secondAttribute="bottom" constant="6" id="ciY-E8-07P"/>
|
||||
<constraint firstAttribute="trailing" secondItem="lbt-Wo-6fc" secondAttribute="trailing" constant="20" id="gMU-gX-3Sg"/>
|
||||
<constraint firstItem="sBT-cU-h7s" firstAttribute="leading" secondItem="7Pv-WL-2Rq" secondAttribute="leading" constant="20" id="nDy-Xc-Ug9"/>
|
||||
<constraint firstItem="MlB-rE-TXV" firstAttribute="leading" secondItem="7Pv-WL-2Rq" secondAttribute="leading" constant="20" id="qb4-Lp-ZMc"/>
|
||||
<constraint firstAttribute="trailing" secondItem="MlB-rE-TXV" secondAttribute="trailing" constant="20" id="v18-62-uee"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<point key="canvasLocation" x="-2" y="6.5"/>
|
||||
<point key="canvasLocation" x="-2" y="16.5"/>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
|
@ -66,7 +66,7 @@ class ElectronDocument: MachineDocument {
|
||||
}
|
||||
|
||||
// MARK: IBActions
|
||||
@IBOutlet var displayTypeButton: NSPopUpButton!
|
||||
@IBOutlet var displayTypeButton: NSPopUpButton?
|
||||
@IBAction func setDisplayType(sender: NSPopUpButton!) {
|
||||
electron.useTelevisionOutput = (sender.indexOfSelectedItem == 1)
|
||||
NSUserDefaults.standardUserDefaults().setInteger(sender.indexOfSelectedItem, forKey: self.displayTypeUserDefaultsKey)
|
||||
@ -82,6 +82,6 @@ class ElectronDocument: MachineDocument {
|
||||
|
||||
let displayType = standardUserDefaults.integerForKey(self.displayTypeUserDefaultsKey)
|
||||
electron.useTelevisionOutput = (displayType == 1)
|
||||
self.displayTypeButton.selectItemAtIndex(displayType)
|
||||
self.displayTypeButton?.selectItemAtIndex(displayType)
|
||||
}
|
||||
}
|
||||
|
@ -176,13 +176,16 @@ class MachineDocument:
|
||||
}
|
||||
|
||||
// MARK: IBActions
|
||||
final func prefixedUserDefaultsKey(key: String) -> String {
|
||||
return "\(self.name).\(key)"
|
||||
}
|
||||
var fastLoadingUserDefaultsKey: String {
|
||||
get {
|
||||
return "\(self.name).fastLoading"
|
||||
return prefixedUserDefaultsKey("fastLoading")
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet var fastLoadingButton: NSButton!
|
||||
@IBOutlet var fastLoadingButton: NSButton?
|
||||
@IBAction func setFastLoading(sender: NSButton!) {
|
||||
if let fastLoadingMachine = machine as? CSFastLoading {
|
||||
let useFastLoadingHack = sender.state == NSOnState
|
||||
@ -200,7 +203,7 @@ class MachineDocument:
|
||||
if let fastLoadingMachine = machine as? CSFastLoading {
|
||||
let useFastLoadingHack = standardUserDefaults.boolForKey(self.fastLoadingUserDefaultsKey)
|
||||
fastLoadingMachine.useFastLoadingHack = useFastLoadingHack
|
||||
self.fastLoadingButton.state = useFastLoadingHack ? NSOnState : NSOffState
|
||||
self.fastLoadingButton?.state = useFastLoadingHack ? NSOnState : NSOffState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,8 @@ class Vic20Document: MachineDocument {
|
||||
if let drive = dataForResource("1540", ofType: "bin", inDirectory: "ROMImages/Commodore1540") {
|
||||
vic20.setDriveROM(drive)
|
||||
}
|
||||
|
||||
establishStoredOptions()
|
||||
}
|
||||
|
||||
override class func autosavesInPlace() -> Bool {
|
||||
@ -66,4 +68,26 @@ class Vic20Document: MachineDocument {
|
||||
override func readFromData(data: NSData, ofType typeName: String) throws {
|
||||
vic20.setPRG(data)
|
||||
}
|
||||
|
||||
@IBOutlet var loadAutomaticallyButton: NSButton?
|
||||
var autoloadingUserDefaultsKey: String {
|
||||
get { return prefixedUserDefaultsKey("autoload") }
|
||||
}
|
||||
@IBAction func setShouldLoadAutomatically(sender: NSButton!) {
|
||||
let loadAutomatically = sender.state == NSOnState
|
||||
vic20.shouldLoadAutomatically = loadAutomatically
|
||||
NSUserDefaults.standardUserDefaults().setBool(loadAutomatically, forKey: self.autoloadingUserDefaultsKey)
|
||||
}
|
||||
override func establishStoredOptions() {
|
||||
super.establishStoredOptions()
|
||||
|
||||
let standardUserDefaults = NSUserDefaults.standardUserDefaults()
|
||||
standardUserDefaults.registerDefaults([
|
||||
autoloadingUserDefaultsKey: true
|
||||
])
|
||||
|
||||
let loadAutomatically = standardUserDefaults.boolForKey(self.autoloadingUserDefaultsKey)
|
||||
vic20.shouldLoadAutomatically = loadAutomatically
|
||||
self.loadAutomaticallyButton?.state = loadAutomatically ? NSOnState : NSOffState
|
||||
}
|
||||
}
|
||||
|
@ -23,5 +23,6 @@
|
||||
- (BOOL)openD64AtURL:(nonnull NSURL *)URL;
|
||||
|
||||
@property (nonatomic, assign) BOOL useFastLoadingHack;
|
||||
@property (nonatomic, assign) BOOL shouldLoadAutomatically;
|
||||
|
||||
@end
|
||||
|
@ -197,4 +197,11 @@ using namespace Commodore::Vic20;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setShouldLoadAutomatically:(BOOL)shouldLoadAutomatically {
|
||||
@synchronized(self) {
|
||||
_shouldLoadAutomatically = shouldLoadAutomatically;
|
||||
_vic20.set_should_automatically_load_media(shouldLoadAutomatically ? true : false);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
Loading…
x
Reference in New Issue
Block a user