1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-06 01:28:57 +00:00

Merge pull request #29 from TomHarte/6560Improvements

Improved 6560 emulation and the Vic-20's bus
This commit is contained in:
Thomas Harte 2016-06-23 20:45:10 -04:00 committed by GitHub
commit 3a4e844a07
3 changed files with 205 additions and 149 deletions

View File

@ -29,62 +29,87 @@ MOS6560::MOS6560() :
"return mix(y, step(yC, 14) * chroma, amplitude);"
"}");
// set up colours table
// 0
// 2, 6, 9, B,
// 4, 5, 8, A, C, E
// 3, 7, D, F
// 1
uint8_t luminances[16] = { // range is 04
0, 4, 1, 3, 2, 2, 1, 3,
2, 1, 2, 1, 2, 3, 2, 3
};
// uint8_t pal_chrominances[16] = { // range is 015; 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,
};
for(int c = 0; c < 16; c++)
{
_colours[c] = (uint8_t)((luminances[c] << 4) | ntsc_chrominances[c]);
}
// 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 04
0, 4, 1, 3, 2, 2, 1, 3,
2, 1, 2, 1, 2, 3, 2, 3
};
uint8_t pal_chrominances[16] = { // range is 015; 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.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.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[address] = value;
_registers.direct_values[address] = value;
switch(address)
{
case 0x0:
_interlaced = !!(value&0x80);
_first_column_location = value & 0x7f;
_registers.interlaced = !!(value&0x80) && _timing.supports_interlacing;
_registers.first_column_location = value & 0x7f;
break;
case 0x1:
_first_row_location = value;
_registers.first_row_location = value;
break;
case 0x2:
_number_of_columns = value & 0x7f;
_video_matrix_start_address = (uint16_t)((_video_matrix_start_address & 0x3c00) | ((value & 0x80) << 2));
_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:
_number_of_rows = (value >> 1)&0x3f;
_tall_characters = !!(value&0x01);
_registers.number_of_rows = (value >> 1)&0x3f;
_registers.tall_characters = !!(value&0x01);
break;
case 0x5:
_character_cell_start_address = (uint16_t)((value & 0x0f) << 10);
_video_matrix_start_address = (uint16_t)((_video_matrix_start_address & 0x0200) | ((value & 0xf0) << 6));
_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:
@ -97,7 +122,7 @@ void MOS6560::set_register(int address, uint8_t value)
case 0xe:
update_audio();
_auxiliary_colour = _colours[value >> 4];
_registers.auxiliary_colour = _colours[value >> 4];
_speaker.set_volume(value & 0xf);
break;
@ -107,12 +132,12 @@ void MOS6560::set_register(int address, uint8_t value)
output_border(_cycles_in_state * 4);
_cycles_in_state = 0;
}
_invertedCells = !((value >> 3)&1);
_borderColour = _colours[value & 0x07];
_backgroundColour = _colours[value >> 4];
_registers.invertedCells = !((value >> 3)&1);
_registers.borderColour = _colours[value & 0x07];
_registers.backgroundColour = _colours[value >> 4];
break;
// TODO: audio, primarily
// TODO: the lightpen, etc
default:
break;
@ -124,8 +149,8 @@ uint8_t MOS6560::get_register(int address)
address &= 0xf;
switch(address)
{
default: return _registers[address];
case 0x03: return (uint8_t)(_vertical_counter << 7) | (_registers[3] & 0x7f);
default: return _registers.direct_values[address];
case 0x03: return (uint8_t)(_vertical_counter << 7) | (_registers.direct_values[3] & 0x7f);
case 0x04: return (_vertical_counter >> 1) & 0xff;
}
}
@ -133,7 +158,7 @@ uint8_t MOS6560::get_register(int address)
void MOS6560::output_border(unsigned int number_of_cycles)
{
uint8_t *colour_pointer = _crt->allocate_write_area(1);
if(colour_pointer) *colour_pointer = _borderColour;
if(colour_pointer) *colour_pointer = _registers.borderColour;
_crt->output_level(number_of_cycles);
}
@ -142,66 +167,104 @@ 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++;
// check for end of scanline
if(_horizontal_counter == 65)
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++;
_column_counter = -1;
if(_vertical_counter == (_interlaced ? (_is_odd_frame ? 262 : 263) : 261))
_vertical_counter ++;
if(_vertical_counter == (_registers.interlaced ? (_is_odd_frame ? 262 : 263) : _timing.lines_per_progressive_field))
{
_is_odd_frame ^= true;
_vertical_counter = 0;
_row_counter = -1;
}
if(_row_counter >= 0)
{
_row_counter++;
if(_row_counter == _number_of_rows*(_tall_characters ? 16 : 8)) _row_counter = -1;
}
else if(_vertical_counter == _first_row_location * 2)
{
_video_matrix_line_address_counter = _video_matrix_start_address;
_row_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;
}
}
if(_column_counter >= 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)
{
_column_counter++;
if(_column_counter == _number_of_columns*2)
{
_column_counter = -1;
if((_row_counter&(_tall_characters ? 15 : 7)) == (_tall_characters ? 15 : 7))
case -1:
if(_horizontal_drawing_latch)
{
_video_matrix_line_address_counter = _video_matrix_address_counter;
_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;
}
}
}
else if(_horizontal_counter == _first_column_location)
{
_column_counter = 0;
_video_matrix_address_counter = _video_matrix_line_address_counter;
}
return fetch_address;
}
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 > 61) _this_state = State::ColourBurst;
else if(_horizontal_counter > 57) _this_state = State::Sync;
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 && _row_counter >= 0) ? State::Pixels : State::Border;
_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 || !_interlaced)) ||
(_interlaced &&
(_vertical_counter < 3 && (_is_odd_frame || !_registers.interlaced)) ||
(_registers.interlaced &&
(
(_vertical_counter == 0 && _horizontal_counter > 32) ||
(_vertical_counter == 1) || (_vertical_counter == 2) ||
@ -231,44 +294,6 @@ uint16_t MOS6560::get_address()
}
_cycles_in_state++;
// compute the address
if(_this_state == State::Pixels)
{
/*
Per http://tinyvga.com/6561 :
The basic video timing is very simple. For
every character the VIC-I is about to display, it first fetches the
character code and colour, then the character appearance (from the
character generator memory). The character codes are read on every
raster line, thus making every line a "bad line". When the raster
beam is outside of the text window, the videochip reads from $001c for
most time. (Some videochips read from $181c instead.) The address
occasionally varies, but it might also be due to a flaky bus. (By
reading from unconnected address space, such as $9100-$910f, you can
read the data fetched by the videochip on the previous clock cycle.)
*/
if(_column_counter&1)
{
return _character_cell_start_address + (_character_code*(_tall_characters ? 16 : 8)) + (_row_counter&(_tall_characters ? 15 : 7));
}
else
{
uint16_t result = _video_matrix_address_counter;
_video_matrix_address_counter++;
return result;
}
}
return 0x1c;
}
void MOS6560::set_graphics_value(uint8_t value, uint8_t colour_value)
{
// TODO: this isn't correct, as _character_value will be
// accessed second, then output will roll over. Probably it's
// correct (given the delays upstream) to output all 8 on an &1
// and to adjust the signalling to the CRT above?
if(_this_state == State::Pixels)
{
if(_column_counter&1)
@ -281,14 +306,14 @@ void MOS6560::set_graphics_value(uint8_t value, uint8_t colour_value)
if(!(_character_colour&0x8))
{
uint8_t colours[2];
if(_invertedCells)
if(_registers.invertedCells)
{
colours[0] = cell_colour;
colours[1] = _backgroundColour;
colours[1] = _registers.backgroundColour;
}
else
{
colours[0] = _backgroundColour;
colours[0] = _registers.backgroundColour;
colours[1] = cell_colour;
}
pixel_pointer[0] = colours[(_character_value >> 7)&1];
@ -302,7 +327,7 @@ void MOS6560::set_graphics_value(uint8_t value, uint8_t colour_value)
}
else
{
uint8_t colours[4] = {_backgroundColour, _borderColour, cell_colour, _auxiliary_colour};
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] =
@ -320,6 +345,8 @@ void MOS6560::set_graphics_value(uint8_t value, uint8_t colour_value)
_character_code = value;
_character_colour = colour_value;
}
_column_counter++;
}
}

View File

@ -28,6 +28,14 @@ class MOS6560 {
Outputs::CRT::CRT *get_crt() { return _crt.get(); }
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);
/*!
Impliedly runs the 6560 for a single cycle, returning the next address that it puts on the bus.
*/
@ -56,6 +64,8 @@ class MOS6560 {
private:
std::unique_ptr<Outputs::CRT::CRT> _crt;
// audio state
class Speaker: public ::Outputs::Filter<Speaker> {
public:
Speaker();
@ -72,38 +82,56 @@ class MOS6560 {
uint8_t _control_registers[4];
uint8_t _volume;
} _speaker;
unsigned int _cycles_since_speaker_update;
void update_audio();
bool _interlaced, _tall_characters;
uint8_t _first_column_location, _first_row_location;
uint8_t _number_of_columns, _number_of_rows;
uint16_t _character_cell_start_address, _video_matrix_start_address;
uint8_t _backgroundColour, _borderColour, _auxiliary_colour;
bool _invertedCells;
// register state
struct {
bool interlaced, tall_characters;
uint8_t first_column_location, first_row_location;
uint8_t number_of_columns, number_of_rows;
uint16_t character_cell_start_address, video_matrix_start_address;
uint8_t backgroundColour, borderColour, auxiliary_colour;
bool invertedCells;
int _horizontal_counter, _vertical_counter;
bool _did_output_graphics;
int _column_counter, _row_counter;
uint16_t _video_matrix_address_counter, _video_matrix_line_address_counter;
uint8_t direct_values[16];
} _registers;
// output state
enum State {
Sync, ColourBurst, Border, Pixels
} _this_state, _output_state;
unsigned int _cycles_in_state;
// counters that cover an entire field
int _horizontal_counter, _vertical_counter;
// latches dictating start and length of drawing
bool _vertical_drawing_latch, _horizontal_drawing_latch;
int _rows_this_field, _columns_this_line;
// current drawing position counter
int _pixel_line_cycle, _column_counter;
int _current_row;
uint16_t _current_character_row;
uint16_t _video_matrix_address_counter, _base_video_matrix_address_counter;
// data latched from the bus
uint8_t _character_code, _character_colour, _character_value;
uint8_t *pixel_pointer;
uint8_t _registers[16];
uint8_t _colours[16];
bool _is_odd_frame;
// lookup table from 6560 colour index to appropriate PAL/NTSC value
uint8_t _colours[16];
uint8_t *pixel_pointer;
void output_border(unsigned int number_of_cycles);
unsigned int _cycles_since_speaker_update;
void update_audio();
struct {
int cycles_per_line;
int lines_per_progressive_field;
bool supports_interlacing;
} _timing;
};
}

View File

@ -47,35 +47,36 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
// run the phase-2 part of the cycle, which is whatever the 6502 said it should be
if(isReadOperation(operation))
{
*value = read_memory(address);
if((address&0xfff0) == 0x9000)
uint8_t result = read_memory(address);
if((address&0xff00) == 0x9000)
{
*value = _mos6560->get_register(address - 0x9000);
result &= _mos6560->get_register(address);
}
else if((address&0xfff0) == 0x9110)
if((address&0xfc10) == 0x9010)
{
*value = _userPortVIA.get_register(address - 0x9110);
result &= _userPortVIA.get_register(address);
}
else if((address&0xfff0) == 0x9120)
if((address&0xfc20) == 0x9020)
{
*value = _keyboardVIA.get_register(address - 0x9120);
result &= _keyboardVIA.get_register(address);
}
*value = result;
}
else
{
uint8_t *ram = ram_pointer(address);
if(ram) *ram = *value;
else if((address&0xfff0) == 0x9000)
if((address&0xff00) == 0x9000)
{
_mos6560->set_register(address - 0x9000, *value);
_mos6560->set_register(address, *value);
}
else if((address&0xfff0) == 0x9110)
if((address&0xfc10) == 0x9010)
{
_userPortVIA.set_register(address - 0x9110, *value);
_userPortVIA.set_register(address, *value);
}
else if((address&0xfff0) == 0x9120)
if((address&0xfc20) == 0x9020)
{
_keyboardVIA.set_register(address - 0x9120, *value);
_keyboardVIA.set_register(address, *value);
}
}