diff --git a/Components/9918/9918.cpp b/Components/9918/9918.cpp index 8c94d72d3..c79f44053 100644 --- a/Components/9918/9918.cpp +++ b/Components/9918/9918.cpp @@ -68,6 +68,14 @@ Base::Base(Personality p) : mode_timing_.end_of_frame_interrupt_position.column = 63; mode_timing_.end_of_frame_interrupt_position.row = 193; } + + // Establish that output is 10 cycles after values have been read. + // This is definitely correct for the TMS; more research may be + // necessary for the other implemented VDPs. + read_pointer_.row = 0; + read_pointer_.column = 0; + write_pointer_.row = 0; + write_pointer_.column = 10; // i.e. 10 cycles ahead of the read pointer. } TMS9918::TMS9918(Personality p): @@ -94,45 +102,44 @@ Outputs::CRT::CRT *TMS9918::get_crt() { return crt_.get(); } -void Base::reset_sprite_collection() { - sprite_set_.sprites_stopped = false; - sprite_set_.fetched_sprite_slot = sprite_set_.active_sprite_slot; - sprite_set_.active_sprite_slot = 0; +void Base::LineBuffer::reset_sprite_collection() { + sprites_stopped = false; + active_sprite_slot = 0; - for(int c = 0; c < sprite_set_.fetched_sprite_slot; ++c) { - sprite_set_.active_sprites[c].shift_position = 0; + for(int c = 0; c < 8; ++c) { + active_sprites[c].shift_position = 0; } } -void Base::posit_sprite(int sprite_number, int sprite_position, int screen_row) { +void Base::posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_position, int screen_row) { if(!(status_ & StatusSpriteOverflow)) { status_ = static_cast((status_ & ~0x1f) | (sprite_number & 0x1f)); } - if(sprite_set_.sprites_stopped) + if(buffer.sprites_stopped) return; // const int sprite_position = ram_[sprite_attribute_table_address_ + static_cast(sprite_number << 2)]; // A sprite Y of 208 means "don't scan the list any further". if(mode_timing_.allow_sprite_terminator && sprite_position == 208) { - sprite_set_.sprites_stopped = true; + buffer.sprites_stopped = true; return; } const int sprite_row = (screen_row - sprite_position)&255; if(sprite_row < 0 || sprite_row >= sprite_height_) return; - if(sprite_set_.active_sprite_slot == mode_timing_.maximum_visible_sprites) { + if(buffer.active_sprite_slot == mode_timing_.maximum_visible_sprites) { status_ |= StatusSpriteOverflow; return; } - SpriteSet::ActiveSprite &sprite = sprite_set_.active_sprites[sprite_set_.active_sprite_slot]; + LineBuffer::ActiveSprite &sprite = buffer.active_sprites[buffer.active_sprite_slot]; sprite.index = sprite_number; sprite.row = sprite_row >> (sprites_magnified_ ? 1 : 0); - ++sprite_set_.active_sprite_slot; + ++buffer.active_sprite_slot; } -void Base::get_sprite_contents(int field, int cycles_left, int screen_row) { +//void Base::get_sprite_contents(int field, int cycles_left, int screen_row) { /* int sprite_id = field / 6; field %= 6; @@ -163,7 +170,7 @@ void Base::get_sprite_contents(int field, int cycles_left, int screen_row) { field = 0; sprite_id++; }*/ -} +//} void TMS9918::run_for(const HalfCycles cycles) { // As specific as I've been able to get: @@ -179,74 +186,185 @@ void TMS9918::run_for(const HalfCycles cycles) { int_cycles >>= 2; if(!int_cycles) return; - while(int_cycles) { - // Determine how much time has passed in the remainder of this line, and proceed. - const int cycles_left = std::min(342 - column_, int_cycles); - const int end_column = column_ + cycles_left; + // There are two intertwined processes here, 'writing' (which means writing to the + // line buffers, i.e. it's everything to do with collecting a line) and 'reading' + // (which means reading from the line buffers and generating video). + int write_cycles_pool = int_cycles; + int read_cycles_pool = int_cycles; + + while(write_cycles_pool || read_cycles_pool) { + if(write_cycles_pool) { + // Determine how much writing to do. + const int write_cycles = std::min(342 - write_pointer_.column, write_cycles_pool); + const int end_column = write_pointer_.column + write_cycles; + LineBuffer &line_buffer = line_buffers_[0];//write_pointer_.row & 1]; - // --------------------------------------- - // Latch scrolling position, if necessary. - // --------------------------------------- - if(column_ < 61 && end_column >= 61) { - if(!row_) { - master_system_.latched_vertical_scroll = master_system_.vertical_scroll; + + // --------------------------------------- + // Latch scrolling position, if necessary. + // --------------------------------------- + if(write_pointer_.column < 61 && end_column >= 61) { + if(!write_pointer_.row) { + master_system_.latched_vertical_scroll = master_system_.vertical_scroll; + } + line_buffer.latched_horizontal_scroll = master_system_.horizontal_scroll; } - master_system_.latched_horizontal_scroll = master_system_.horizontal_scroll; - } - // ------------------------ - // Perform memory accesses. - // ------------------------ + + // ------------------------ + // Perform memory accesses. + // ------------------------ #define fetch(function) \ - if(end_column < 171) { \ + if(final_window < 171) { \ function(first_window, final_window);\ } else {\ function(first_window, final_window);\ } - // column_ and end_column are in 342-per-line cycles; - // adjust them to a count of windows. - const int first_window = column_ >> 1; - const int final_window = end_column >> 1; - if(first_window != final_window) { - switch(line_mode_) { - case LineMode::Text: fetch(fetch_tms_text); break; - case LineMode::Character: fetch(fetch_tms_character); break; - case LineMode::SMS: fetch(fetch_sms); break; - case LineMode::Refresh: fetch(fetch_tms_refresh); break; + // column_ and end_column are in 342-per-line cycles; + // adjust them to a count of windows. + const int first_window = write_pointer_.column >> 1; + const int final_window = end_column >> 1; + if(first_window != final_window) { + switch(line_buffer.line_mode) { + case LineMode::Text: fetch(fetch_tms_text); break; + case LineMode::Character: fetch(fetch_tms_character); break; + case LineMode::SMS: fetch(fetch_sms); break; + case LineMode::Refresh: fetch(fetch_tms_refresh); break; + } } - } #undef fetch - // -------------------- - // Output video stream. - // -------------------- + + // ------------------------------- + // Check for interrupt conditions. + // ------------------------------- + if(write_pointer_.column < mode_timing_.line_interrupt_position && end_column >= mode_timing_.line_interrupt_position) { + // The Sega VDP offers a decrementing counter for triggering line interrupts; + // it is reloaded either when it overflows or upon every non-pixel line after the first. + // It is otherwise decremented. + if(is_sega_vdp(personality_)) { + if(write_pointer_.row >= 0 && write_pointer_.row <= mode_timing_.pixel_lines) { + --line_interrupt_counter; + if(line_interrupt_counter == 0xff) { + line_interrupt_pending_ = true; + line_interrupt_counter = line_interrupt_target; + } + } else { + line_interrupt_counter = line_interrupt_target; + } + } + + // TODO: the V9938 provides line interrupts from direct specification of the target line. + // So life is easy. + } + + if( + write_pointer_.row == mode_timing_.end_of_frame_interrupt_position.row && + write_pointer_.column < mode_timing_.end_of_frame_interrupt_position.column && + end_column >= mode_timing_.end_of_frame_interrupt_position.column + ) { + status_ |= StatusInterrupt; + } + + + + // ------------- + // Advance time. + // ------------- + write_pointer_.column = end_column; + write_cycles_pool -= write_cycles; + + if(write_pointer_.column == 342) { + write_pointer_.column = 0; + write_pointer_.row = (write_pointer_.row + 1) % mode_timing_.total_lines; + line_buffer = line_buffers_[0];//write_pointer_.row & 1]; + + // Establish the output mode for the next line. + set_current_screen_mode(); + + // Based on the output mode, pick a line mode. + line_buffer.first_pixel_output_column = 86; + line_buffer.next_border_column = 342; + mode_timing_.maximum_visible_sprites = 4; + switch(screen_mode_) { + case ScreenMode::Text: + line_buffer.line_mode = LineMode::Text; + line_buffer.first_pixel_output_column = 94; + line_buffer.next_border_column = 334; + break; + case ScreenMode::SMSMode4: + line_buffer.line_mode = LineMode::SMS; + mode_timing_.maximum_visible_sprites = 8; + break; + default: + line_buffer.line_mode = LineMode::Character; + break; + } + + if( + (screen_mode_ == ScreenMode::Blank) || + (write_pointer_.row >= mode_timing_.pixel_lines && write_pointer_.row != mode_timing_.total_lines-1)) + line_buffer.line_mode = LineMode::Refresh; + } + } + + + if(read_cycles_pool) { + // Determine how much time has passed in the remainder of this line, and proceed. + const int read_cycles = std::min(342 - read_pointer_.column, read_cycles_pool); + const int end_column = read_pointer_.column + read_cycles; + LineBuffer &line_buffer = line_buffers_[0];//read_pointer_.row & 1]; + + + + // -------------------- + // Output video stream. + // -------------------- #define intersect(left, right, code) \ { \ - const int start = std::max(column_, left); \ + const int start = std::max(read_pointer_.column, left); \ const int end = std::min(end_column, right); \ if(end > start) {\ code;\ }\ } - if(line_mode_ == LineMode::Refresh || row_ > mode_timing_.pixel_lines) { - if(row_ >= mode_timing_.first_vsync_line && row_ < mode_timing_.first_vsync_line+4) { - // Vertical sync. - if(end_column == 342) { - crt_->output_sync(342 * 4); + if(line_buffer.line_mode == LineMode::Refresh) { + if(read_pointer_.row >= mode_timing_.first_vsync_line && read_pointer_.row < mode_timing_.first_vsync_line+4) { + // Vertical sync. + if(end_column == 342) { + crt_->output_sync(342 * 4); + } + } else { + // Right border. + intersect(0, 15, output_border(end - start)); + + // Blanking region; total length is 58 cycles, + // and 58+15 = 73. So output the lot when the + // cursor passes 73. + if(read_pointer_.column < 73 && end_column >= 73) { + crt_->output_blank(8*4); + crt_->output_sync(26*4); + crt_->output_blank(2*4); + crt_->output_default_colour_burst(14*4); + crt_->output_blank(8*4); + } + + // Border colour for the rest of the line. + intersect(73, 342, output_border(end - start)); } } else { // Right border. intersect(0, 15, output_border(end - start)); // Blanking region. - if(column_ < 73 && end_column >= 73) { + if(read_pointer_.column < 73 && end_column >= 73) { crt_->output_blank(8*4); crt_->output_sync(26*4); crt_->output_blank(2*4); @@ -254,141 +372,58 @@ void TMS9918::run_for(const HalfCycles cycles) { crt_->output_blank(8*4); } - // Most of line. - intersect(73, 342, output_border(end - start)); - } - } else { - // Right border. - intersect(0, 15, output_border(end - start)); + // Left border. + intersect(73, line_buffer.first_pixel_output_column, output_border(end - start)); - // Blanking region. - if(column_ < 73 && end_column >= 73) { - crt_->output_blank(8*4); - crt_->output_sync(26*4); - crt_->output_blank(2*4); - crt_->output_default_colour_burst(14*4); - crt_->output_blank(8*4); - } - - // Left border. - intersect(73, mode_timing_.first_pixel_output_column, output_border(end - start)); - - // Pixel region. - intersect( - mode_timing_.first_pixel_output_column, - mode_timing_.next_border_column, - if(start == mode_timing_.first_pixel_output_column) { - pixel_origin_ = pixel_target_ = reinterpret_cast( - crt_->allocate_write_area(static_cast(mode_timing_.next_border_column - mode_timing_.first_pixel_output_column) + 8) // TODO: the +8 is really for the SMS only; make it optional. - ); - } - - if(pixel_target_) { - const int relative_start = start - mode_timing_.first_pixel_output_column; - const int relative_end = end - mode_timing_.first_pixel_output_column; - switch(line_mode_) { - case LineMode::SMS: draw_sms(relative_start, relative_end); break; - case LineMode::Character: draw_tms_character(relative_start, relative_end); break; - case LineMode::Text: draw_tms_text(relative_start, relative_end); break; - - case LineMode::Refresh: break; /* Dealt with elsewhere. */ + // Pixel region. + intersect( + line_buffer.first_pixel_output_column, + line_buffer.next_border_column, + if(start == line_buffer.first_pixel_output_column) { + pixel_origin_ = pixel_target_ = reinterpret_cast( + crt_->allocate_write_area(static_cast(line_buffer.next_border_column - line_buffer.first_pixel_output_column)) + ); } - } - if(end == mode_timing_.next_border_column) { - const unsigned int length = static_cast(mode_timing_.next_border_column - mode_timing_.first_pixel_output_column); - crt_->output_data(length * 4, length); - pixel_origin_ = pixel_target_ = nullptr; - } - ); + if(pixel_target_) { + const int relative_start = start - line_buffer.first_pixel_output_column; + const int relative_end = end - line_buffer.first_pixel_output_column; + switch(line_buffer.line_mode) { + case LineMode::SMS: draw_sms(relative_start, relative_end); break; + case LineMode::Character: draw_tms_character(relative_start, relative_end); break; + case LineMode::Text: draw_tms_text(relative_start, relative_end); break; - // Additional right border, if called for. - if(mode_timing_.next_border_column != 342) { - intersect(mode_timing_.next_border_column, 342, output_border(end - start)); - } - } - -#undef intersect - - // ----------------- - // End video stream. - // ----------------- - - - - // ------------------------------- - // Check for interrupt conditions. - // ------------------------------- - - if(column_ < mode_timing_.line_interrupt_position && end_column >= mode_timing_.line_interrupt_position) { - // The Sega VDP offers a decrementing counter for triggering line interrupts; - // it is reloaded either when it overflows or upon every non-pixel line after the first. - // It is otherwise decremented. - if(is_sega_vdp(personality_)) { - if(row_ >= 0 && row_ <= mode_timing_.pixel_lines) { - --line_interrupt_counter; - if(line_interrupt_counter == 0xff) { - line_interrupt_pending_ = true; - line_interrupt_counter = line_interrupt_target; + case LineMode::Refresh: break; /* Dealt with elsewhere. */ + } } - } else { - line_interrupt_counter = line_interrupt_target; + + if(end == line_buffer.next_border_column) { + const unsigned int length = static_cast(line_buffer.next_border_column - line_buffer.first_pixel_output_column); + crt_->output_data(length * 4, length); + pixel_origin_ = pixel_target_ = nullptr; + } + ); + + // Additional right border, if called for. + if(line_buffer.next_border_column != 342) { + intersect(line_buffer.next_border_column, 342, output_border(end - start)); } } - // TODO: the V9938 provides line interrupts from direct specification of the target line. - // So life is easy. - } - - if( - row_ == mode_timing_.end_of_frame_interrupt_position.row && - column_ < mode_timing_.end_of_frame_interrupt_position.column && - end_column >= mode_timing_.end_of_frame_interrupt_position.column - ) { - status_ |= StatusInterrupt; - } + #undef intersect - // ------------- - // Advance time. - // ------------- + // ------------- + // Advance time. + // ------------- + read_pointer_.column = end_column; + read_cycles_pool -= read_cycles; - column_ = end_column; // column_ is now the column that has been reached in this line. - int_cycles -= cycles_left; // Count down duration to run for. - - - - // ----------------------------------- - // Prepare for next line, potentially. - // ----------------------------------- - if(column_ == 342) { - column_ = 0; - row_ = (row_ + 1) % mode_timing_.total_lines; - - // Establish the output mode for the next line. - set_current_mode(); - - // Based on the output mode, pick a line mode. - mode_timing_.first_pixel_output_column = 86; - mode_timing_.next_border_column = 342; - mode_timing_.maximum_visible_sprites = 4; - switch(screen_mode_) { - case ScreenMode::Text: - line_mode_ = LineMode::Text; - mode_timing_.first_pixel_output_column = 94; - mode_timing_.next_border_column = 334; - break; - case ScreenMode::SMSMode4: - line_mode_ = LineMode::SMS; - mode_timing_.maximum_visible_sprites = 8; - break; - default: - line_mode_ = LineMode::Character; - break; + if(read_pointer_.column == 342) { + read_pointer_.column = 0; + read_pointer_.row = (read_pointer_.row + 1) % mode_timing_.total_lines; } - - if((screen_mode_ == ScreenMode::Blank) || (row_ >= mode_timing_.pixel_lines && row_ != mode_timing_.total_lines-1)) line_mode_ = LineMode::Refresh; } } } @@ -537,7 +572,10 @@ void TMS9918::set_register(int address, uint8_t value) { uint8_t TMS9918::get_current_line() { // Determine the row to return. static const int row_change_position = 62; // This is the proper Master System value; substitute if any other VDPs turn out to have this functionality. - int source_row = (column_ < row_change_position) ? (row_ + mode_timing_.total_lines - 1)%mode_timing_.total_lines : row_; + int source_row = + (write_pointer_.column < row_change_position) + ? (write_pointer_.row + mode_timing_.total_lines - 1)%mode_timing_.total_lines + : write_pointer_.row; // This assumes NTSC 192-line. TODO: other modes. if(source_row >= 0xdb) source_row -= 6; @@ -568,7 +606,7 @@ uint8_t TMS9918::get_latched_horizontal_counter() { } void TMS9918::latch_horizontal_counter() { - latched_column_ = column_; + latched_column_ = write_pointer_.column; } uint8_t TMS9918::get_register(int address) { @@ -603,7 +641,7 @@ HalfCycles TMS9918::get_time_until_interrupt() { const int time_until_frame_interrupt = ( ((mode_timing_.end_of_frame_interrupt_position.row * 342) + mode_timing_.end_of_frame_interrupt_position.column + frame_length) - - ((row_ * 342) + column_) + ((write_pointer_.row * 342) + write_pointer_.column) ) % frame_length; if(!enable_line_interrupts_) return half_cycles_before_internal_cycles(time_until_frame_interrupt); @@ -615,8 +653,8 @@ HalfCycles TMS9918::get_time_until_interrupt() { // If there is still time for a line interrupt this frame, that'll be it; // otherwise it'll be on the next frame, supposing there's ever time for // it at all. - if(row_+line_interrupt_counter <= mode_timing_.pixel_lines) { - next_line_interrupt_row = row_+line_interrupt_counter; + if(write_pointer_.row+line_interrupt_counter <= mode_timing_.pixel_lines) { + next_line_interrupt_row = write_pointer_.row+line_interrupt_counter; } else { if(line_interrupt_target <= mode_timing_.pixel_lines) next_line_interrupt_row = mode_timing_.total_lines + line_interrupt_target; @@ -633,9 +671,9 @@ HalfCycles TMS9918::get_time_until_interrupt() { // Figure out the number of internal cycles until the next line interrupt, which is the amount // of time to the next tick over and then next_line_interrupt_row - row_ lines further. - int local_cycles_until_next_tick = (mode_timing_.line_interrupt_position - column_ + 342) % 342; + int local_cycles_until_next_tick = (mode_timing_.line_interrupt_position - write_pointer_.column + 342) % 342; if(!local_cycles_until_next_tick) local_cycles_until_next_tick += 342; - const int local_cycles_until_line_interrupt = local_cycles_until_next_tick + (next_line_interrupt_row - row_) * 342; + const int local_cycles_until_line_interrupt = local_cycles_until_next_tick + (next_line_interrupt_row - write_pointer_.row) * 342; if(!generate_interrupts_) return half_cycles_before_internal_cycles(time_until_frame_interrupt); @@ -782,11 +820,12 @@ void Base::draw_tms_character(int start, int end) { } void Base::draw_tms_text(int start, int end) { + LineBuffer &line_buffer = line_buffers_[0];//read_pointer_.row & 1]; const uint32_t colours[2] = { palette[background_colour_], palette[text_colour_] }; const int shift = start % 6; int byte_column = start / 6; - int pattern = reverse_table.map[pattern_buffer_[byte_column]] >> shift; + int pattern = reverse_table.map[line_buffer.patterns[byte_column][0]] >> shift; int pixels_left = end - start; int length = std::min(pixels_left, 6 - shift); while(true) { @@ -800,11 +839,12 @@ void Base::draw_tms_text(int start, int end) { if(!pixels_left) break; length = std::min(6, pixels_left); byte_column++; - pattern = reverse_table.map[pattern_buffer_[byte_column]]; + pattern = reverse_table.map[line_buffer.patterns[byte_column][0]]; } } void Base::draw_sms(int start, int end) { + LineBuffer &line_buffer = line_buffers_[0];//read_pointer_.row & 1]; int colour_buffer[256]; /* @@ -812,15 +852,15 @@ void Base::draw_sms(int start, int end) { */ int tile_start = start, tile_end = end; int tile_offset = start; - if(row_ >= 16 || !master_system_.horizontal_scroll_lock) { - for(int c = start; c < (master_system_.latched_horizontal_scroll & 7); ++c) { + if(read_pointer_.row >= 16 || !master_system_.horizontal_scroll_lock) { + for(int c = start; c < (line_buffer.latched_horizontal_scroll & 7); ++c) { colour_buffer[c] = 16 + background_colour_; ++tile_offset; } // Remove the border area from that to which tiles will be drawn. - tile_start = std::max(start - (master_system_.latched_horizontal_scroll & 7), 0); - tile_end = std::max(end - (master_system_.latched_horizontal_scroll & 7), 0); + tile_start = std::max(start - (line_buffer.latched_horizontal_scroll & 7), 0); + tile_end = std::max(end - (line_buffer.latched_horizontal_scroll & 7), 0); } @@ -838,15 +878,15 @@ void Base::draw_sms(int start, int end) { int pixels_left = tile_end - tile_start; int length = std::min(pixels_left, 8 - shift); - pattern = *reinterpret_cast(master_system_.tile_graphics[byte_column]); - if(master_system_.names[byte_column].flags&2) + pattern = *reinterpret_cast(line_buffer.patterns[byte_column]); + if(line_buffer.names[byte_column].flags&2) pattern >>= shift; else pattern <<= shift; while(true) { - const int palette_offset = (master_system_.names[byte_column].flags&0x18) << 1; - if(master_system_.names[byte_column].flags&2) { + const int palette_offset = (line_buffer.names[byte_column].flags&0x18) << 1; + if(line_buffer.names[byte_column].flags&2) { for(int c = 0; c < length; ++c) { colour_buffer[tile_offset] = ((pattern_index[3] & 0x01) << 3) | @@ -875,21 +915,21 @@ void Base::draw_sms(int start, int end) { length = std::min(8, pixels_left); byte_column++; - pattern = *reinterpret_cast(master_system_.tile_graphics[byte_column]); + pattern = *reinterpret_cast(line_buffer.patterns[byte_column]); } } /* Apply sprites (if any). */ - if(sprite_set_.fetched_sprite_slot) { + if(line_buffer.active_sprite_slot) { int sprite_buffer[256]; int sprite_collision = 0; memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(int)); // Draw all sprites into the sprite buffer. - for(int index = sprite_set_.fetched_sprite_slot - 1; index >= 0; --index) { - SpriteSet::ActiveSprite &sprite = sprite_set_.active_sprites[index]; + for(int index = line_buffer.active_sprite_slot - 1; index >= 0; --index) { + LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; if(sprite.shift_position < 16) { const int pixel_start = std::max(start, sprite.x); diff --git a/Components/9918/Implementation/9918Base.hpp b/Components/9918/Implementation/9918Base.hpp index af2a2b1ee..fb9da9745 100644 --- a/Components/9918/Implementation/9918Base.hpp +++ b/Components/9918/Implementation/9918Base.hpp @@ -97,20 +97,24 @@ class Base { bool generate_interrupts_ = false; int sprite_height_ = 8; - size_t pattern_name_address_ = 0; - size_t colour_table_address_ = 0; - size_t pattern_generator_table_address_ = 0; - size_t sprite_attribute_table_address_ = 0; - size_t sprite_generator_table_address_ = 0; + size_t pattern_name_address_ = 0; // i.e. address of the tile map. + size_t colour_table_address_ = 0; // address of the colour map (if applicable). + size_t pattern_generator_table_address_ = 0; // address of the tile contents. + size_t sprite_attribute_table_address_ = 0; // address of the sprite list. + size_t sprite_generator_table_address_ = 0; // address of the sprite contents. uint8_t text_colour_ = 0; uint8_t background_colour_ = 0; - // Internal mechanisms for position tracking. - int column_ = 0, row_ = 0, latched_column_ = 0; + // This implementation of this chip officially accepts a 3.58Mhz clock, but runs + // internally at 5.37Mhz. The following two help to maintain a lossless conversion + // from the one to the other. int cycles_error_ = 0; HalfCycles half_cycles_before_internal_cycles(int internal_cycles); + // Internal mechanisms for position tracking. + int latched_column_ = 0; + // A helper function to output the current border colour for // the number of cycles supplied. void output_border(int cycles); @@ -130,25 +134,6 @@ class Base { int pixel_lines = 192; int first_vsync_line = 227; - /* - Horizontal layout (on a 342-cycle clock): - - 15 cycles right border - 58 cycles blanking & sync - 13 cycles left border - - ... i.e. to cycle 86, then: - - border up to first_pixel_output_column; - pixels up to next_border_column; - border up to the end. - - e.g. standard 256-pixel modes will want to set - first_pixel_output_column = 86, next_border_column = 342. - */ - int first_pixel_output_column; - int next_border_column; - // Maximum number of sprite slots to populate; // if sprites beyond this number should be visible // then the appropriate status information will be set. @@ -172,18 +157,91 @@ class Base { bool enable_line_interrupts_ = false; bool line_interrupt_pending_ = false; - // The line mode describes the proper timing diagram for the current line. + // The screen mode is a necessary predecessor to picking the line mode, + // which is the thing latched per line. + enum class ScreenMode { + Blank, + Text, + MultiColour, + ColouredText, + Graphics, + SMSMode4 + } screen_mode_; + enum class LineMode { Text, Character, Refresh, SMS - } line_mode_ = LineMode::Text; + }; // Temporary buffers collect a representation of this line prior to pixel serialisation. - uint8_t pattern_names_[40]; - uint8_t pattern_buffer_[40]; - uint8_t colour_buffer_[40]; + struct LineBuffer { + // The line mode describes the proper timing diagram for this line. + LineMode line_mode = LineMode::Text; + + // Holds the horizontal scroll position to apply to this line; + // of those VDPs currently implemented, affects the Master System only. + uint8_t latched_horizontal_scroll = 0; + + // The names array holds pattern names, as an offset into memory, and + // potentially flags also. + struct { + size_t offset; + uint8_t flags; + } names[40]; + + // The patterns array holds tile patterns, corresponding 1:1 with names. + // Four bytes per pattern is the maximum required by any + // currently-implemented VDP. + uint8_t patterns[40][4]; + + /* + Horizontal layout (on a 342-cycle clock): + + 15 cycles right border + 58 cycles blanking & sync + 13 cycles left border + + ... i.e. to cycle 86, then: + + border up to first_pixel_output_column; + pixels up to next_border_column; + border up to the end. + + e.g. standard 256-pixel modes will want to set + first_pixel_output_column = 86, next_border_column = 342. + */ + int first_pixel_output_column; + int next_border_column; + + // An active sprite is one that has been selected for composition onto + // this line. + struct ActiveSprite { + int index = 0; // The original in-table index of this sprite. + int row = 0; // The row of the sprite that should be drawn. + int x = 0; // The sprite's x position on screen. + + uint8_t image[4]; // Up to four bytes of image information. + int shift_position = 0; // An offset representing how much of the image information has already been drawn. + } active_sprites[8]; + + int active_sprite_slot = 0; // A pointer to the slot into which a new active sprite will be deposited, if required. + bool sprites_stopped = false; // A special TMS feature is that a sentinel value can be used to prevent any further sprites + // being evaluated for display. This flag determines whether the sentinel has yet been reached. + + void reset_sprite_collection(); + } line_buffers_[2]; + void posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_y, int screen_row); + + // There is a delay between reading into the line buffer and outputting from there to the screen. That delay + // is observeable because reading time affects availability of memory accesses and therefore time in which + // to update sprites and tiles, but writing time affects when the palette is used and when the collision flag + // may end up being set. So the two processes are slightly decoupled. The end of reading one line may overlap + // with the beginning of writing the next, hence the two separate line buffers. + struct LineBufferPointer { + int row, column; + } read_pointer_, write_pointer_; // Extra information that affects the Master System output mode. struct { @@ -200,50 +258,12 @@ class Base { uint32_t colour_ram[32]; bool cram_is_selected = false; - // Temporary buffers for a line of Master System graphics, - // and latched scrolling offsets. - struct { - size_t offset; - uint8_t flags; - } names[32]; - uint8_t tile_graphics[32][4]; + // Holds the vertical scroll position for this frame; this is latched + // once and cannot dynamically be changed until the next frame. uint8_t latched_vertical_scroll = 0; - uint8_t latched_horizontal_scroll = 0; } master_system_; - // Holds results of sprite data fetches that occur on this - // line. Therefore has to contain: up to four or eight sets - // of sprite data for this line, and its horizontal position, - // plus a growing list of which sprites are selected for - // the next line. - struct SpriteSet { - struct ActiveSprite { - int index = 0; - int row = 0; - - uint8_t image[4]; - int x = 0; - int shift_position = 0; - } active_sprites[8]; - - int active_sprite_slot = 0; - int fetched_sprite_slot = 0; - bool sprites_stopped = false; - } sprite_set_; - - inline void reset_sprite_collection(); - inline void posit_sprite(int sprite_number, int sprite_y, int screen_row); - inline void get_sprite_contents(int start, int cycles, int screen_row); - - enum class ScreenMode { - Blank, - Text, - MultiColour, - ColouredText, - Graphics, - SMSMode4 - } screen_mode_; - void set_current_mode() { + void set_current_screen_mode() { if(blank_display_) { screen_mode_ = ScreenMode::Blank; return; @@ -283,7 +303,6 @@ class Base { screen_mode_ = ScreenMode::Blank; } - void do_external_slot() { switch(queued_access_) { default: return; @@ -307,36 +326,7 @@ class Base { queued_access_ = MemoryAccess::None; } -#define slot(n) \ - if(use_end && end+1 == n) return;\ - case n - -#define external_slot(n) \ - slot(n): do_external_slot(); - -#define external_slots_2(n) \ - external_slot(n); \ - external_slot(n+1); - -#define external_slots_4(n) \ - external_slots_2(n); \ - external_slots_2(n+2); - -#define external_slots_8(n) \ - external_slots_4(n); \ - external_slots_4(n+4); - -#define external_slots_16(n) \ - external_slots_8(n); \ - external_slots_8(n+8); - -#define external_slots_32(n) \ - external_slots_16(n); \ - external_slots_16(n+16); - /* - TODO: explain offset by four windows in data gathering. - Fetching routines follow below; they obey the following rules: 1) input is a start position and an end position; they should perform the proper @@ -368,30 +358,59 @@ class Base { for the exceptions. */ +#define slot(n) \ + if(use_end && end+1 == n) return;\ + case n + +#define external_slot(n) \ + slot(n): do_external_slot(); + +#define external_slots_2(n) \ + external_slot(n); \ + external_slot(n+1); + +#define external_slots_4(n) \ + external_slots_2(n); \ + external_slots_2(n+2); + +#define external_slots_8(n) \ + external_slots_4(n); \ + external_slots_4(n+4); + +#define external_slots_16(n) \ + external_slots_8(n); \ + external_slots_8(n+8); + +#define external_slots_32(n) \ + external_slots_16(n); \ + external_slots_16(n+16); + /*********************************************** TMS9918 Fetching Code ************************************************/ template void fetch_tms_refresh(int start, int end) { -#define refresh(location) external_slot(location+1) +#define refresh(location) \ + slot(location): \ + external_slot(location+1); #define refreshes_2(location) \ - refresh(location); \ + refresh(location); \ refresh(location+2); #define refreshes_4(location) \ - refreshes_2(location); \ + refreshes_2(location); \ refreshes_2(location+4); #define refreshes_8(location) \ - refreshes_4(location); \ + refreshes_4(location); \ refreshes_4(location+8); switch(start) { - default: + default: assert(false); - /* 44 external slots (= 44 windows) */ + /* 44 external slots */ external_slots_32(0) external_slots_8(32) external_slots_4(40) @@ -416,8 +435,8 @@ class Base { } template void fetch_tms_text(int start, int end) { -#define fetch_tile_name(location, column) slot(location): pattern_names_[column] = ram_[row_base + column]; -#define fetch_tile_pattern(location, column) slot(location): pattern_buffer_[column] = ram_[row_offset + size_t(pattern_names_[column] << 3)]; +#define fetch_tile_name(location, column) slot(location): line_buffer.names[column].offset = ram_[row_base + column]; +#define fetch_tile_pattern(location, column) slot(location): line_buffer.patterns[column][0] = ram_[row_offset + size_t(line_buffer.names[column].offset << 3)]; #define fetch_column(location, column) \ fetch_tile_name(location, column); \ @@ -436,11 +455,12 @@ class Base { fetch_columns_4(location, column); \ fetch_columns_4(location+12, column+4); - const size_t row_base = pattern_name_address_ & (0x3c00 | static_cast(row_ >> 3) * 40); - const size_t row_offset = pattern_generator_table_address_ & (0x3800 | (row_ & 7)); + LineBuffer &line_buffer = line_buffers_[0];//write_pointer_.row & 1]; + const size_t row_base = pattern_name_address_ & (0x3c00 | static_cast(write_pointer_.row >> 3) * 40); + const size_t row_offset = pattern_generator_table_address_ & (0x3800 | (write_pointer_.row & 7)); switch(start) { - default: + default: assert(false); /* 47 external slots (= 47 windows) */ external_slots_32(0) @@ -456,8 +476,9 @@ class Base { fetch_columns_8(119, 24); fetch_columns_8(143, 32); - /* 4 more external slots */ + /* 5 more external slots */ external_slots_4(167); + external_slot(171); return; } @@ -492,11 +513,11 @@ class Base { #define sprite_y_read(location, sprite) \ slot(location): -#define fetch_tile_name(column) pattern_names_[column] = ram_[(row_base + column) & 0x3fff]; +#define fetch_tile_name(column) line_buffer.names[column].offset = ram_[(row_base + column) & 0x3fff]; #define fetch_tile(column) {\ - colour_buffer_[column] = ram_[(colour_base + static_cast((pattern_names_[column] << 3) >> colour_name_shift)) & 0x3fff]; \ - pattern_buffer_[column] = ram_[(pattern_base + static_cast(pattern_names_[column] << 3)) & 0x3fff]; \ + line_buffer.patterns[column][0] = ram_[(colour_base + static_cast((line_buffer.names[column].offset << 3) >> colour_name_shift)) & 0x3fff]; \ + line_buffer.patterns[column][1] = ram_[(pattern_base + static_cast(line_buffer.names[column].offset << 3)) & 0x3fff]; \ } #define background_fetch_block(location, column) \ @@ -517,7 +538,8 @@ class Base { slot(location+14): \ slot(location+15): fetch_tile(column+3) - const size_t row_base = pattern_name_address_ + static_cast((row_ << 2)&~31); + LineBuffer &line_buffer = line_buffers_[0];//write_pointer_.row & 1]; + const size_t row_base = pattern_name_address_ + static_cast((write_pointer_.row << 2)&~31); size_t pattern_base = pattern_generator_table_address_; size_t colour_base = colour_table_address_; @@ -525,21 +547,22 @@ class Base { if(screen_mode_ == ScreenMode::Graphics) { // If this is high resolution mode, allow the row number to affect the pattern and colour addresses. - pattern_base &= static_cast(0x2000 | ((row_ & 0xc0) << 5)); - colour_base &= static_cast(0x2000 | ((row_ & 0xc0) << 5)); + pattern_base &= static_cast(0x2000 | ((write_pointer_.row & 0xc0) << 5)); + colour_base &= static_cast(0x2000 | ((write_pointer_.row & 0xc0) << 5)); - colour_base += static_cast(row_ & 7); + colour_base += static_cast(write_pointer_.row & 7); colour_name_shift = 0; } if(screen_mode_ == ScreenMode::MultiColour) { - pattern_base += static_cast((row_ >> 2) & 7); + pattern_base += static_cast((write_pointer_.row >> 2) & 7); } else { - pattern_base += static_cast(row_ & 7); + pattern_base += static_cast(write_pointer_.row & 7); } switch(start) { - default: + default: assert(false); + external_slots_2(0); sprite_fetch_block(2, 0); @@ -591,18 +614,18 @@ class Base { template void fetch_sms(int start, int end) { #define sprite_fetch(sprite) {\ - sprite_set_.active_sprites[sprite].x = \ + line_buffer.active_sprites[sprite].x = \ ram_[\ - sprite_attribute_table_address_ & size_t(0x3f80 | (sprite_set_.active_sprites[sprite].index << 1))\ + sprite_attribute_table_address_ & size_t(0x3f80 | (line_buffer.active_sprites[sprite].index << 1))\ ] - (master_system_.shift_sprites_8px_left ? 8 : 0); \ const uint8_t name = ram_[\ - sprite_attribute_table_address_ & size_t(0x3f81 | (sprite_set_.active_sprites[sprite].index << 1))\ + sprite_attribute_table_address_ & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 1))\ ] & (sprites_16x16_ ? ~1 : ~0);\ - const size_t graphic_location = sprite_generator_table_address_ & size_t(0x2000 | (name << 5) | (sprite_set_.active_sprites[sprite].row << 2)); \ - sprite_set_.active_sprites[sprite].image[0] = ram_[graphic_location]; \ - sprite_set_.active_sprites[sprite].image[1] = ram_[graphic_location+1]; \ - sprite_set_.active_sprites[sprite].image[2] = ram_[graphic_location+2]; \ - sprite_set_.active_sprites[sprite].image[3] = ram_[graphic_location+3]; \ + const size_t graphic_location = sprite_generator_table_address_ & size_t(0x2000 | (name << 5) | (line_buffer.active_sprites[sprite].row << 2)); \ + line_buffer.active_sprites[sprite].image[0] = ram_[graphic_location]; \ + line_buffer.active_sprites[sprite].image[1] = ram_[graphic_location+1]; \ + line_buffer.active_sprites[sprite].image[2] = ram_[graphic_location+2]; \ + line_buffer.active_sprites[sprite].image[3] = ram_[graphic_location+3]; \ } #define sprite_fetch_block(location, sprite) \ @@ -617,23 +640,23 @@ class Base { #define sprite_y_read(location, sprite) \ slot(location): \ - posit_sprite(sprite, ram_[sprite_attribute_table_address_ & (sprite | 0x3f00)], row_); \ - posit_sprite(sprite+1, ram_[sprite_attribute_table_address_ & ((sprite + 1) | 0x3f00)], row_); \ + posit_sprite(line_buffer, sprite, ram_[sprite_attribute_table_address_ & (sprite | 0x3f00)], write_pointer_.row); \ + posit_sprite(line_buffer, sprite+1, ram_[sprite_attribute_table_address_ & ((sprite + 1) | 0x3f00)], write_pointer_.row); \ #define fetch_tile_name(column, row_info) {\ const size_t scrolled_column = (column - horizontal_offset) & 0x1f;\ const size_t address = row_info.pattern_address_base + (scrolled_column << 1); \ - master_system_.names[column].flags = ram_[address+1]; \ - master_system_.names[column].offset = static_cast( \ - (((master_system_.names[column].flags&1) << 8) | ram_[address]) << 5 \ - ) + row_info.sub_row[(master_system_.names[column].flags&4) >> 2]; \ + line_buffer.names[column].flags = ram_[address+1]; \ + line_buffer.names[column].offset = static_cast( \ + (((line_buffer.names[column].flags&1) << 8) | ram_[address]) << 5 \ + ) + row_info.sub_row[(line_buffer.names[column].flags&4) >> 2]; \ } #define fetch_tile(column) \ - master_system_.tile_graphics[column][0] = ram_[master_system_.names[column].offset]; \ - master_system_.tile_graphics[column][1] = ram_[master_system_.names[column].offset+1]; \ - master_system_.tile_graphics[column][2] = ram_[master_system_.names[column].offset+2]; \ - master_system_.tile_graphics[column][3] = ram_[master_system_.names[column].offset+3]; + line_buffer.patterns[column][0] = ram_[line_buffer.names[column].offset]; \ + line_buffer.patterns[column][1] = ram_[line_buffer.names[column].offset+1]; \ + line_buffer.patterns[column][2] = ram_[line_buffer.names[column].offset+2]; \ + line_buffer.patterns[column][3] = ram_[line_buffer.names[column].offset+3]; #define background_fetch_block(location, column, sprite, row_info) \ slot(location): fetch_tile_name(column, row_info) \ @@ -660,11 +683,12 @@ class Base { slot(location+15): fetch_tile(column+3) // Determine the coarse horizontal scrolling offset; this isn't applied on the first two lines if the programmer has requested it. - const int horizontal_offset = (row_ >= 16 || !master_system_.horizontal_scroll_lock) ? (master_system_.latched_horizontal_scroll >> 3) : 0; + LineBuffer &line_buffer = line_buffers_[0];//write_pointer_ .row & 1]; + const int horizontal_offset = (write_pointer_.row >= 16 || !master_system_.horizontal_scroll_lock) ? (line_buffer.latched_horizontal_scroll >> 3) : 0; // Determine row info for the screen both (i) if vertical scrolling is applied; and (ii) if it isn't. // The programmer can opt out of applying vertical scrolling to the right-hand portion of the display. - const int scrolled_row = (row_ + master_system_.latched_vertical_scroll) % 224; + const int scrolled_row = (write_pointer_.row + master_system_.latched_vertical_scroll) % 224; struct RowInfo { size_t pattern_address_base; size_t sub_row[2]; @@ -675,15 +699,14 @@ class Base { }; RowInfo row_info; if(master_system_.vertical_scroll_lock) { - row_info.pattern_address_base = pattern_name_address_ & static_cast(((row_ & ~7) << 3) | 0x3800); - row_info.sub_row[0] = size_t((row_ & 7) << 2); - row_info.sub_row[1] = 28 ^ size_t((row_ & 7) << 2); + row_info.pattern_address_base = pattern_name_address_ & static_cast(((write_pointer_.row & ~7) << 3) | 0x3800); + row_info.sub_row[0] = size_t((write_pointer_.row & 7) << 2); + row_info.sub_row[1] = 28 ^ size_t((write_pointer_.row & 7) << 2); } else row_info = scrolled_row_info; // ... and do the actual fetching, which follows this routine: switch(start) { - default: - assert(false); + default: assert(false); sprite_fetch_block(0, 0); sprite_fetch_block(6, 2); @@ -695,7 +718,7 @@ class Base { sprite_fetch_block(23, 6); slot(29): - reset_sprite_collection(); + line_buffer.reset_sprite_collection(); do_external_slot(); external_slot(30);