diff --git a/Analyser/Static/Sega/Target.hpp b/Analyser/Static/Sega/Target.hpp index 3eceb774d..24230293d 100644 --- a/Analyser/Static/Sega/Target.hpp +++ b/Analyser/Static/Sega/Target.hpp @@ -48,7 +48,9 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl= Analyser::Static::Sega::Target::Model::MasterSystem +constexpr bool is_master_system(Analyser::Static::Sega::Target::Model model) { + return model >= Analyser::Static::Sega::Target::Model::MasterSystem; +} } } diff --git a/Components/9918/9918.cpp b/Components/9918/9918.cpp deleted file mode 100644 index aef510c59..000000000 --- a/Components/9918/9918.cpp +++ /dev/null @@ -1,1052 +0,0 @@ -// -// 9918.cpp -// Clock Signal -// -// Created by Thomas Harte on 25/11/2017. -// Copyright 2017 Thomas Harte. All rights reserved. -// - -#include "9918.hpp" - -#include -#include -#include -#include "../../Outputs/Log.hpp" - -using namespace TI::TMS; - -namespace { - -constexpr uint8_t StatusInterrupt = 0x80; -constexpr uint8_t StatusSpriteOverflow = 0x40; - -constexpr int StatusSpriteCollisionShift = 5; -constexpr uint8_t StatusSpriteCollision = 0x20; - -// 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole -// line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368. -constexpr unsigned int CRTCyclesPerLine = 1365; -constexpr unsigned int CRTCyclesDivider = 4; - -struct ReverseTable { - std::uint8_t map[256]; - - ReverseTable() { - for(int c = 0; c < 256; ++c) { - map[c] = uint8_t( - ((c & 0x80) >> 7) | - ((c & 0x40) >> 5) | - ((c & 0x20) >> 3) | - ((c & 0x10) >> 1) | - ((c & 0x08) << 1) | - ((c & 0x04) << 3) | - ((c & 0x02) << 5) | - ((c & 0x01) << 7) - ); - } - } -} reverse_table; - -} - -Base::Base(Personality p) : - personality_(p), - crt_(CRTCyclesPerLine, CRTCyclesDivider, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red8Green8Blue8) { - // Unimaginatively, this class just passes RGB through to the shader. Investigation is needed - // into whether there's a more natural form. It feels unlikely given the diversity of chips modelled. - - switch(p) { - case TI::TMS::TMS9918A: - case TI::TMS::SMSVDP: - case TI::TMS::SMS2VDP: - case TI::TMS::GGVDP: - ram_.resize(16 * 1024); - break; - case TI::TMS::V9938: - ram_.resize(128 * 1024); - break; - case TI::TMS::V9958: - ram_.resize(192 * 1024); - break; - } - - if(is_sega_vdp(personality_)) { - mode_timing_.line_interrupt_position = 64; - - mode_timing_.end_of_frame_interrupt_position.column = 63; - mode_timing_.end_of_frame_interrupt_position.row = 193; - } - - // Establish that output is delayed after reading by `output_lag` cycles; start - // at a random position. - read_pointer_.row = rand() % 262; - read_pointer_.column = rand() % (342 - output_lag); - write_pointer_.row = read_pointer_.row; - write_pointer_.column = read_pointer_.column + output_lag; -} - -TMS9918::TMS9918(Personality p): - Base(p) { - crt_.set_display_type(Outputs::Display::DisplayType::RGB); - crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f)); - - // The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement - // intended to produce the correct relationship between the hard edges between pixels and - // the colour clock. It was eyeballed rather than derived from any knowledge of the TMS - // colour burst generator because I've yet to find any. - crt_.set_immediate_default_phase(0.85f); -} - -void TMS9918::set_tv_standard(TVStandard standard) { - tv_standard_ = standard; - switch(standard) { - case TVStandard::PAL: - mode_timing_.total_lines = 313; - mode_timing_.first_vsync_line = 253; - crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::PAL50); - break; - default: - mode_timing_.total_lines = 262; - mode_timing_.first_vsync_line = 227; - crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::NTSC60); - break; - } -} - -void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) { - crt_.set_scan_target(scan_target); -} - -Outputs::Display::ScanStatus TMS9918::get_scaled_scan_status() const { - // The input was scaled by 3/4 to convert half cycles to internal ticks, - // so undo that and also allow for: (i) the multiply by 4 that it takes - // to reach the CRT; and (ii) the fact that the half-cycles value was scaled, - // and this should really reply in whole cycles. - return crt_.get_scaled_scan_status() * (4.0f / (3.0f * 8.0f)); -} - -void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) { - crt_.set_display_type(display_type); -} - -Outputs::Display::DisplayType TMS9918::get_display_type() const { - return crt_.get_display_type(); -} - -void Base::LineBuffer::reset_sprite_collection() { - sprites_stopped = false; - active_sprite_slot = 0; - - for(int c = 0; c < 8; ++c) { - active_sprites[c].shift_position = 0; - } -} - -void Base::posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_position, int screen_row) { - if(!(status_ & StatusSpriteOverflow)) { - status_ = uint8_t((status_ & ~0x1f) | (sprite_number & 0x1f)); - } - if(buffer.sprites_stopped) - return; - - // A sprite Y of 208 means "don't scan the list any further". - if(mode_timing_.allow_sprite_terminator && sprite_position == mode_timing_.sprite_terminator) { - buffer.sprites_stopped = true; - return; - } - - const int sprite_row = (((screen_row + 1) % mode_timing_.total_lines) - ((sprite_position + 1) & 255)) & 255; - if(sprite_row < 0 || sprite_row >= sprite_height_) return; - - if(buffer.active_sprite_slot == mode_timing_.maximum_visible_sprites) { - status_ |= StatusSpriteOverflow; - return; - } - - LineBuffer::ActiveSprite &sprite = buffer.active_sprites[buffer.active_sprite_slot]; - sprite.index = sprite_number; - sprite.row = sprite_row >> (sprites_magnified_ ? 1 : 0); - ++buffer.active_sprite_slot; -} - -void TMS9918::run_for(const HalfCycles cycles) { - // As specific as I've been able to get: - // Scanline time is always 228 cycles. - // PAL output is 313 lines total. NTSC output is 262 lines total. - // Interrupt is signalled upon entering the lower border. - - // Convert 456 clocked half cycles per line to 342 internal cycles per line; - // the internal clock is 1.5 times the nominal 3.579545 Mhz that I've advertised - // for this part. So multiply by three quarters. - int int_cycles = int(cycles.as_integral() * 3) + cycles_error_; - cycles_error_ = int_cycles & 3; - int_cycles >>= 2; - if(!int_cycles) return; - - // 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) { -#ifndef NDEBUG - LineBufferPointer backup = read_pointer_; -#endif - - 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_[write_pointer_.row]; - - // Determine what this does to any enqueued VRAM access. - minimum_access_column_ = write_pointer_.column + cycles_until_access_; - cycles_until_access_ -= write_cycles; - - - // --------------------------------------- - // Latch scrolling position, if necessary. - // --------------------------------------- - if(is_sega_vdp(personality_)) { - if(write_pointer_.column < 61 && end_column >= 61) { - if(!write_pointer_.row) { - master_system_.latched_vertical_scroll = master_system_.vertical_scroll; - - if(master_system_.mode4_enable) { - mode_timing_.pixel_lines = 192; - if(mode2_enable_ && mode1_enable_) mode_timing_.pixel_lines = 224; - if(mode2_enable_ && mode3_enable_) mode_timing_.pixel_lines = 240; - - mode_timing_.allow_sprite_terminator = mode_timing_.pixel_lines == 192; - mode_timing_.first_vsync_line = (mode_timing_.total_lines + mode_timing_.pixel_lines) >> 1; - - mode_timing_.end_of_frame_interrupt_position.row = mode_timing_.pixel_lines + 1; - } - } - line_buffer.latched_horizontal_scroll = master_system_.horizontal_scroll; - } - } - - - - // ------------------------ - // Perform memory accesses. - // ------------------------ -#define fetch(function) \ - 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 = 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 - - - - // ------------------------------- - // 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; - LineBuffer &next_line_buffer = line_buffers_[write_pointer_.row]; - - // Establish the output mode for the next line. - set_current_screen_mode(); - - // Based on the output mode, pick a line mode. - next_line_buffer.first_pixel_output_column = 86; - next_line_buffer.next_border_column = 342; - mode_timing_.maximum_visible_sprites = 4; - switch(screen_mode_) { - case ScreenMode::Text: - next_line_buffer.line_mode = LineMode::Text; - next_line_buffer.first_pixel_output_column = 94; - next_line_buffer.next_border_column = 334; - break; - case ScreenMode::SMSMode4: - next_line_buffer.line_mode = LineMode::SMS; - mode_timing_.maximum_visible_sprites = 8; - break; - default: - next_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)) - next_line_buffer.line_mode = LineMode::Refresh; - } - } - - -#ifndef NDEBUG - assert(backup.row == read_pointer_.row && backup.column == read_pointer_.column); - backup = write_pointer_; -#endif - - - if(read_cycles_pool) { - // Determine how much time has passed in the remainder of this line, and proceed. - const int target_read_cycles = std::min(342 - read_pointer_.column, read_cycles_pool); - int read_cycles_performed = 0; - uint32_t next_cram_value = 0; - - while(read_cycles_performed < target_read_cycles) { - const uint32_t cram_value = next_cram_value; - next_cram_value = 0; - int read_cycles = target_read_cycles - read_cycles_performed; - if(!upcoming_cram_dots_.empty() && upcoming_cram_dots_.front().location.row == read_pointer_.row) { - int time_until_dot = upcoming_cram_dots_.front().location.column - read_pointer_.column; - - if(time_until_dot < read_cycles) { - read_cycles = time_until_dot; - next_cram_value = upcoming_cram_dots_.front().value; - upcoming_cram_dots_.erase(upcoming_cram_dots_.begin()); - } - } - - if(!read_cycles) continue; - read_cycles_performed += read_cycles; - - const int end_column = read_pointer_.column + read_cycles; - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; - - - // -------------------- - // Output video stream. - // -------------------- - -#define intersect(left, right, code) { \ - const int start = std::max(read_pointer_.column, left); \ - const int end = std::min(end_column, right); \ - if(end > start) {\ - code;\ - }\ - } - -#define border(left, right) intersect(left, right, output_border(end - start, cram_value)) - - if(line_buffer.line_mode == LineMode::Refresh || read_pointer_.row > mode_timing_.pixel_lines) { - 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. - border(0, 15); - - // 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. - border(73, 342); - } - } else { - // Right border. - border(0, 15); - - // Blanking region. - 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); - } - - // Left border. - border(73, line_buffer.first_pixel_output_column); - - // Pixel region. - intersect( - line_buffer.first_pixel_output_column, - line_buffer.next_border_column, - if(!asked_for_write_area_) { - asked_for_write_area_ = true; - pixel_origin_ = pixel_target_ = reinterpret_cast( - crt_.begin_data(size_t(line_buffer.next_border_column - line_buffer.first_pixel_output_column)) - ); - } - - 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, cram_value); 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. */ - } - } - - if(end == line_buffer.next_border_column) { - const int length = line_buffer.next_border_column - line_buffer.first_pixel_output_column; - crt_.output_data(length * 4, size_t(length)); - pixel_origin_ = pixel_target_ = nullptr; - asked_for_write_area_ = false; - } - ); - - // Additional right border, if called for. - if(line_buffer.next_border_column != 342) { - border(line_buffer.next_border_column, 342); - } - } - -#undef border -#undef intersect - - - - // ------------- - // Advance time. - // ------------- - read_pointer_.column = end_column; - } - - read_cycles_pool -= target_read_cycles; - if(read_pointer_.column == 342) { - read_pointer_.column = 0; - read_pointer_.row = (read_pointer_.row + 1) % mode_timing_.total_lines; - } - } - - assert(backup.row == write_pointer_.row && backup.column == write_pointer_.column); - } -} - -void Base::output_border(int cycles, uint32_t cram_dot) { - cycles *= 4; - uint32_t border_colour = - is_sega_vdp(personality_) ? - master_system_.colour_ram[16 + background_colour_] : - palette[background_colour_]; - - if(cram_dot) { - uint32_t *const pixel_target = reinterpret_cast(crt_.begin_data(1)); - if(pixel_target) { - *pixel_target = border_colour | cram_dot; - } - crt_.output_level(4); - cycles -= 4; - } - - if(cycles) { - // If the border colour is 0, that can be communicated - // more efficiently as an explicit blank. - if(border_colour) { - uint32_t *const pixel_target = reinterpret_cast(crt_.begin_data(1)); - if(pixel_target) { - *pixel_target = border_colour; - } - crt_.output_level(cycles); - } else { - crt_.output_blank(cycles); - } - } -} - -void TMS9918::write(int address, uint8_t value) { - // Writes to address 0 are writes to the video RAM. Store - // the value and return. - if(!(address & 1)) { - write_phase_ = false; - - // Enqueue the write to occur at the next available slot. - read_ahead_buffer_ = value; - queued_access_ = MemoryAccess::Write; - cycles_until_access_ = vram_access_delay(); - - return; - } - - // Writes to address 1 are performed in pairs; if this is the - // low byte of a value, store it and wait for the high byte. - if(!write_phase_) { - low_write_ = value; - write_phase_ = true; - - // The initial write should half update the access pointer. - ram_pointer_ = (ram_pointer_ & 0xff00) | low_write_; - return; - } - - // The RAM pointer is always set on a second write, regardless of - // whether the caller is intending to enqueue a VDP operation. - ram_pointer_ = (ram_pointer_ & 0x00ff) | uint16_t(value << 8); - - write_phase_ = false; - if(value & 0x80) { - if(is_sega_vdp(personality_)) { - if(value & 0x40) { - master_system_.cram_is_selected = true; - return; - } - value &= 0xf; - } else { - value &= 0x7; - } - - // This is a write to a register. - switch(value) { - case 0: - if(is_sega_vdp(personality_)) { - master_system_.vertical_scroll_lock = !!(low_write_ & 0x80); - master_system_.horizontal_scroll_lock = !!(low_write_ & 0x40); - master_system_.hide_left_column = !!(low_write_ & 0x20); - enable_line_interrupts_ = !!(low_write_ & 0x10); - master_system_.shift_sprites_8px_left = !!(low_write_ & 0x08); - master_system_.mode4_enable = !!(low_write_ & 0x04); - } - mode2_enable_ = !!(low_write_ & 0x02); - break; - - case 1: - blank_display_ = !(low_write_ & 0x40); - generate_interrupts_ = !!(low_write_ & 0x20); - mode1_enable_ = !!(low_write_ & 0x10); - mode3_enable_ = !!(low_write_ & 0x08); - sprites_16x16_ = !!(low_write_ & 0x02); - sprites_magnified_ = !!(low_write_ & 0x01); - - sprite_height_ = 8; - if(sprites_16x16_) sprite_height_ <<= 1; - if(sprites_magnified_) sprite_height_ <<= 1; - break; - - case 2: - pattern_name_address_ = size_t((low_write_ & 0xf) << 10) | 0x3ff; - master_system_.pattern_name_address = pattern_name_address_ | ((personality_ == TMS::SMSVDP) ? 0x000 : 0x400); - break; - - case 3: - colour_table_address_ = size_t(low_write_ << 6) | 0x3f; - break; - - case 4: - pattern_generator_table_address_ = size_t((low_write_ & 0x07) << 11) | 0x7ff; - break; - - case 5: - sprite_attribute_table_address_ = size_t((low_write_ & 0x7f) << 7) | 0x7f; - master_system_.sprite_attribute_table_address = sprite_attribute_table_address_ | ((personality_ == TMS::SMSVDP) ? 0x00 : 0x80); - break; - - case 6: - sprite_generator_table_address_ = size_t((low_write_ & 0x07) << 11) | 0x7ff; - master_system_.sprite_generator_table_address = sprite_generator_table_address_ | ((personality_ == TMS::SMSVDP) ? 0x0000 : 0x1800); - break; - - case 7: - text_colour_ = low_write_ >> 4; - background_colour_ = low_write_ & 0xf; - break; - - case 8: - if(is_sega_vdp(personality_)) { - master_system_.horizontal_scroll = low_write_; - } - break; - - case 9: - if(is_sega_vdp(personality_)) { - master_system_.vertical_scroll = low_write_; - } - break; - - case 10: - if(is_sega_vdp(personality_)) { - line_interrupt_target = low_write_; - } - break; - - default: - LOG("Unknown TMS write: " << int(low_write_) << " to " << int(value)); - break; - } - } else { - // This is an access via the RAM pointer. - if(!(value & 0x40)) { - // A read request is enqueued upon setting the address; conversely a write - // won't be enqueued unless and until some actual data is supplied. - queued_access_ = MemoryAccess::Read; - cycles_until_access_ = vram_access_delay(); - } - master_system_.cram_is_selected = false; - } -} - -uint8_t TMS9918::get_current_line() { - // Determine the row to return. - constexpr int row_change_position = 63; // This is the proper Master System value; substitute if any other VDPs turn out to have this functionality. - int source_row = - (write_pointer_.column < row_change_position) - ? (write_pointer_.row + mode_timing_.total_lines - 1)%mode_timing_.total_lines - : write_pointer_.row; - - if(tv_standard_ == TVStandard::NTSC) { - if(mode_timing_.pixel_lines == 240) { - // NTSC 256x240: 00-FF, 00-06 - } else if(mode_timing_.pixel_lines == 224) { - // NTSC 256x224: 00-EA, E5-FF - if(source_row >= 0xeb) source_row -= 6; - } else { - // NTSC 256x192: 00-DA, D5-FF - if(source_row >= 0xdb) source_row -= 6; - } - } else { - if(mode_timing_.pixel_lines == 240) { - // PAL 256x240: 00-FF, 00-0A, D2-FF - if(source_row >= 267) source_row -= 0x39; - } else if(mode_timing_.pixel_lines == 224) { - // PAL 256x224: 00-FF, 00-02, CA-FF - if(source_row >= 259) source_row -= 0x39; - } else { - // PAL 256x192: 00-F2, BA-FF - if(source_row >= 0xf3) source_row -= 0x39; - } - } - - return uint8_t(source_row); -} - -uint8_t TMS9918::get_latched_horizontal_counter() { - // Translate from internal numbering, which puts pixel output - // in the final 256 pixels of 342, to the public numbering, - // which makes the 256 pixels the first 256 spots, but starts - // counting at -48, and returns only the top 8 bits of the number. - int public_counter = latched_column_ - 86; - if(public_counter < -46) public_counter += 342; - return uint8_t(public_counter >> 1); -} - -void TMS9918::latch_horizontal_counter() { - latched_column_ = write_pointer_.column; -} - -uint8_t TMS9918::read(int address) { - write_phase_ = false; - - // Reads from address 0 read video RAM, via the read-ahead buffer. - if(!(address & 1)) { - // Enqueue the write to occur at the next available slot. - uint8_t result = read_ahead_buffer_; - queued_access_ = MemoryAccess::Read; - return result; - } - - // Reads from address 1 get the status register. - uint8_t result = status_; - status_ &= ~(StatusInterrupt | StatusSpriteOverflow | StatusSpriteCollision); - line_interrupt_pending_ = false; - return result; -} - -HalfCycles Base::half_cycles_before_internal_cycles(int internal_cycles) { - return HalfCycles(((internal_cycles << 2) + (2 - cycles_error_)) / 3); -} - -HalfCycles TMS9918::get_next_sequence_point() { - if(!generate_interrupts_ && !enable_line_interrupts_) return HalfCycles::max(); - if(get_interrupt_line()) return HalfCycles::max(); - - // Calculate the amount of time until the next end-of-frame interrupt. - const int frame_length = 342 * mode_timing_.total_lines; - int time_until_frame_interrupt = - ( - ((mode_timing_.end_of_frame_interrupt_position.row * 342) + mode_timing_.end_of_frame_interrupt_position.column + frame_length) - - ((write_pointer_.row * 342) + write_pointer_.column) - ) % frame_length; - if(!time_until_frame_interrupt) time_until_frame_interrupt = frame_length; - - if(!enable_line_interrupts_) return half_cycles_before_internal_cycles(time_until_frame_interrupt); - - // Calculate when the next line interrupt will occur. - int next_line_interrupt_row = -1; - - int cycles_to_next_interrupt_threshold = mode_timing_.line_interrupt_position - write_pointer_.column; - int line_of_next_interrupt_threshold = write_pointer_.row; - if(cycles_to_next_interrupt_threshold <= 0) { - cycles_to_next_interrupt_threshold += 342; - ++line_of_next_interrupt_threshold; - } - - if(is_sega_vdp(personality_)) { - // 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(line_of_next_interrupt_threshold + line_interrupt_counter <= mode_timing_.pixel_lines) { - next_line_interrupt_row = line_of_next_interrupt_threshold + line_interrupt_counter; - } else { - if(line_interrupt_target <= mode_timing_.pixel_lines) - next_line_interrupt_row = mode_timing_.total_lines + line_interrupt_target; - } - } - - // If there's actually no interrupt upcoming, despite being enabled, either return - // the frame end interrupt or no interrupt pending as appropriate. - if(next_line_interrupt_row == -1) { - return generate_interrupts_ ? - half_cycles_before_internal_cycles(time_until_frame_interrupt) : - HalfCycles::max(); - } - - // 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. - const int local_cycles_until_line_interrupt = cycles_to_next_interrupt_threshold + (next_line_interrupt_row - line_of_next_interrupt_threshold) * 342; - if(!generate_interrupts_) return half_cycles_before_internal_cycles(local_cycles_until_line_interrupt); - - // Return whichever interrupt is closer. - return half_cycles_before_internal_cycles(std::min(local_cycles_until_line_interrupt, time_until_frame_interrupt)); -} - -HalfCycles TMS9918::get_time_until_line(int line) { - if(line < 0) line += mode_timing_.total_lines; - - int cycles_to_next_interrupt_threshold = mode_timing_.line_interrupt_position - write_pointer_.column; - int line_of_next_interrupt_threshold = write_pointer_.row; - if(cycles_to_next_interrupt_threshold <= 0) { - cycles_to_next_interrupt_threshold += 342; - ++line_of_next_interrupt_threshold; - } - - if(line_of_next_interrupt_threshold > line) { - line += mode_timing_.total_lines; - } - - return half_cycles_before_internal_cycles(cycles_to_next_interrupt_threshold + (line - line_of_next_interrupt_threshold)*342); -} - -bool TMS9918::get_interrupt_line() { - return ((status_ & StatusInterrupt) && generate_interrupts_) || (enable_line_interrupts_ && line_interrupt_pending_); -} - -// MARK: - - -void Base::draw_tms_character(int start, int end) { - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; - - // Paint the background tiles. - const int pixels_left = end - start; - if(screen_mode_ == ScreenMode::MultiColour) { - for(int c = start; c < end; ++c) { - pixel_target_[c] = palette[ - (line_buffer.patterns[c >> 3][0] >> (((c & 4)^4))) & 15 - ]; - } - } else { - const int shift = start & 7; - int byte_column = start >> 3; - - int length = std::min(pixels_left, 8 - shift); - - int pattern = reverse_table.map[line_buffer.patterns[byte_column][0]] >> shift; - uint8_t colour = line_buffer.patterns[byte_column][1]; - uint32_t colours[2] = { - palette[(colour & 15) ? (colour & 15) : background_colour_], - palette[(colour >> 4) ? (colour >> 4) : background_colour_] - }; - - int background_pixels_left = pixels_left; - while(true) { - background_pixels_left -= length; - for(int c = 0; c < length; ++c) { - pixel_target_[c] = colours[pattern&0x01]; - pattern >>= 1; - } - pixel_target_ += length; - - if(!background_pixels_left) break; - length = std::min(8, background_pixels_left); - byte_column++; - - pattern = reverse_table.map[line_buffer.patterns[byte_column][0]]; - colour = line_buffer.patterns[byte_column][1]; - colours[0] = palette[(colour & 15) ? (colour & 15) : background_colour_]; - colours[1] = palette[(colour >> 4) ? (colour >> 4) : background_colour_]; - } - } - - // Paint sprites and check for collisions, but only if at least one sprite is active - // on this line. - if(line_buffer.active_sprite_slot) { - const int shift_advance = sprites_magnified_ ? 1 : 2; - // If this is the start of the line clip any part of any sprites that is off to the left. - if(!start) { - for(int index = 0; index < line_buffer.active_sprite_slot; ++index) { - LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; - if(sprite.x < 0) sprite.shift_position -= shift_advance * sprite.x; - } - } - - int sprite_buffer[256]; - int sprite_collision = 0; - memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); - - constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; - constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; - - // Draw all sprites into the sprite buffer. - const int shifter_target = sprites_16x16_ ? 32 : 16; - for(int index = line_buffer.active_sprite_slot - 1; index >= 0; --index) { - LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; - if(sprite.shift_position < shifter_target) { - const int pixel_start = std::max(start, sprite.x); - for(int c = pixel_start; c < end && sprite.shift_position < shifter_target; ++c) { - const int shift = (sprite.shift_position >> 1) ^ 7; - int sprite_colour = (sprite.image[shift >> 3] >> (shift & 7)) & 1; - - // A colision is detected regardless of sprite colour ... - sprite_collision |= sprite_buffer[c] & sprite_colour; - sprite_buffer[c] |= sprite_colour; - - // ... but a sprite with the transparent colour won't actually be visible. - sprite_colour &= colour_masks[sprite.image[2]&15]; - pixel_origin_[c] = - (pixel_origin_[c] & sprite_colour_selection_masks[sprite_colour^1]) | - (palette[sprite.image[2]&15] & sprite_colour_selection_masks[sprite_colour]); - - sprite.shift_position += shift_advance; - } - } - } - - status_ |= sprite_collision << StatusSpriteCollisionShift; - } -} - -void Base::draw_tms_text(int start, int end) { - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; - 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[line_buffer.patterns[byte_column][0]] >> shift; - int pixels_left = end - start; - int length = std::min(pixels_left, 6 - shift); - while(true) { - pixels_left -= length; - for(int c = 0; c < length; ++c) { - pixel_target_[c] = colours[pattern&0x01]; - pattern >>= 1; - } - pixel_target_ += length; - - if(!pixels_left) break; - length = std::min(6, pixels_left); - byte_column++; - pattern = reverse_table.map[line_buffer.patterns[byte_column][0]]; - } -} - -void Base::draw_sms(int start, int end, uint32_t cram_dot) { - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; - int colour_buffer[256]; - - /* - Add extra border for any pixels that fall before the fine scroll. - */ - int tile_start = start, tile_end = end; - int tile_offset = start; - 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 - (line_buffer.latched_horizontal_scroll & 7), 0); - tile_end = std::max(end - (line_buffer.latched_horizontal_scroll & 7), 0); - } - - - uint32_t pattern; - uint8_t *const pattern_index = reinterpret_cast(&pattern); - - /* - Add background tiles; these will fill the colour_buffer with values in which - the low five bits are a palette index, and bit six is set if this tile has - priority over sprites. - */ - if(tile_start < end) { - const int shift = tile_start & 7; - int byte_column = tile_start >> 3; - int pixels_left = tile_end - tile_start; - int length = std::min(pixels_left, 8 - shift); - - 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 = (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) | - ((pattern_index[2] & 0x01) << 2) | - ((pattern_index[1] & 0x01) << 1) | - ((pattern_index[0] & 0x01) << 0) | - palette_offset; - ++tile_offset; - pattern >>= 1; - } - } else { - for(int c = 0; c < length; ++c) { - colour_buffer[tile_offset] = - ((pattern_index[3] & 0x80) >> 4) | - ((pattern_index[2] & 0x80) >> 5) | - ((pattern_index[1] & 0x80) >> 6) | - ((pattern_index[0] & 0x80) >> 7) | - palette_offset; - ++tile_offset; - pattern <<= 1; - } - } - - pixels_left -= length; - if(!pixels_left) break; - - length = std::min(8, pixels_left); - byte_column++; - pattern = *reinterpret_cast(line_buffer.patterns[byte_column]); - } - } - - /* - Apply sprites (if any). - */ - if(line_buffer.active_sprite_slot) { - const int shift_advance = sprites_magnified_ ? 1 : 2; - - // If this is the start of the line clip any part of any sprites that is off to the left. - if(!start) { - for(int index = 0; index < line_buffer.active_sprite_slot; ++index) { - LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; - if(sprite.x < 0) sprite.shift_position -= shift_advance * sprite.x; - } - } - - int sprite_buffer[256]; - int sprite_collision = 0; - memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); - - // Draw all sprites into the sprite buffer. - 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); - - // TODO: it feels like the work below should be simplifiable; - // the double shift in particular, and hopefully the variable shift. - for(int c = pixel_start; c < end && sprite.shift_position < 16; ++c) { - const int shift = (sprite.shift_position >> 1); - const int sprite_colour = - (((sprite.image[3] << shift) & 0x80) >> 4) | - (((sprite.image[2] << shift) & 0x80) >> 5) | - (((sprite.image[1] << shift) & 0x80) >> 6) | - (((sprite.image[0] << shift) & 0x80) >> 7); - - if(sprite_colour) { - sprite_collision |= sprite_buffer[c]; - sprite_buffer[c] = sprite_colour | 0x10; - } - - sprite.shift_position += shift_advance; - } - } - } - - // Draw the sprite buffer onto the colour buffer, wherever the tile map doesn't have - // priority (or is transparent). - for(int c = start; c < end; ++c) { - if( - sprite_buffer[c] && - (!(colour_buffer[c]&0x20) || !(colour_buffer[c]&0xf)) - ) colour_buffer[c] = sprite_buffer[c]; - } - - if(sprite_collision) - status_ |= StatusSpriteCollision; - } - - // Map from the 32-colour buffer to real output pixels, applying the specific CRAM dot if any. - pixel_target_[start] = master_system_.colour_ram[colour_buffer[start] & 0x1f] | cram_dot; - for(int c = start+1; c < end; ++c) { - pixel_target_[c] = master_system_.colour_ram[colour_buffer[c] & 0x1f]; - } - - // If the VDP is set to hide the left column and this is the final call that'll come - // this line, hide it. - if(end == 256) { - if(master_system_.hide_left_column) { - pixel_origin_[0] = pixel_origin_[1] = pixel_origin_[2] = pixel_origin_[3] = - pixel_origin_[4] = pixel_origin_[5] = pixel_origin_[6] = pixel_origin_[7] = - master_system_.colour_ram[16 + background_colour_]; - } - } -} diff --git a/Components/9918/9918.hpp b/Components/9918/9918.hpp index 806977e31..a3a80ea66 100644 --- a/Components/9918/9918.hpp +++ b/Components/9918/9918.hpp @@ -12,13 +12,38 @@ #include "../../Outputs/CRT/CRT.hpp" #include "../../ClockReceiver/ClockReceiver.hpp" -#include "Implementation/9918Base.hpp" - #include namespace TI { namespace TMS { +enum Personality { + TMS9918A, // includes the 9928 and 9929; set TV standard and output device as desired. + V9938, + V9958, + + // Sega extensions. + SMSVDP, + SMS2VDP, + GGVDP, + MDVDP, +}; + +enum class TVStandard { + /*! i.e. 50Hz output at around 312.5 lines/field */ + PAL, + /*! i.e. 60Hz output at around 262.5 lines/field */ + NTSC +}; + +} +} + +#include "Implementation/9918Base.hpp" + +namespace TI { +namespace TMS { + /*! Provides emulation of the TMS9918a, TMS9928 and TMS9929. Likely in the future to be the vessel for emulation of sufficiently close derivatives, such as the Master System VDP. @@ -30,13 +55,10 @@ namespace TMS { These chips have only one non-on-demand interaction with the outside world: an interrupt line. See get_time_until_interrupt and get_interrupt_line for asynchronous operation options. */ -class TMS9918: public Base { +template class TMS9918: private Base { public: - /*! - Constructs an instance of the drive controller that behaves according to personality @c p. - @param p The type of controller to emulate. - */ - TMS9918(Personality p); + /*! Constructs an instance of the VDP that behaves according to the templated personality. */ + TMS9918(); /*! Sets the TV standard for this TMS, if that is hard-coded in hardware. */ void set_tv_standard(TVStandard standard); @@ -44,18 +66,24 @@ class TMS9918: public Base { /*! Sets the scan target this TMS will post content to. */ void set_scan_target(Outputs::Display::ScanTarget *); - /// Gets the current scan status. + /*! Gets the current scan status. */ Outputs::Display::ScanStatus get_scaled_scan_status() const; - /*! Sets the type of display the CRT will request. */ + /*! Sets the type of CRT display. */ void set_display_type(Outputs::Display::DisplayType); - /*! Gets the type of display the CRT will request. */ + /*! Gets the type of CRT display. */ Outputs::Display::DisplayType get_display_type() const; /*! - Runs the VCP for the number of cycles indicate; it is an implicit assumption of the code - that the input clock rate is 3579545 Hz, the NTSC colour clock rate. + Runs the VDP for the number of cycles indicate; the input clock rate is implicitly assumed. + + For everything except the Mega Drive VDP: + * the input clock rate should be 3579545 Hz, the NTSC colour clock rate. + + For the Mega Drive: + * the input clock rate should be around 7.6MHz; 15/7ths of the NTSC colour + clock rate for NTSC output and 12/7ths of the PAL colour clock rate for PAL output. */ void run_for(const HalfCycles cycles); @@ -65,11 +93,11 @@ class TMS9918: public Base { /*! Gets a register value. */ uint8_t read(int address); - /*! Gets the current scan line; provided by the Master System only. */ - uint8_t get_current_line(); + /*! Gets the current scan line; provided by the Sega VDPs only. */ + uint8_t get_current_line() const; - /*! Gets the current latched horizontal counter; provided by the Master System only. */ - uint8_t get_latched_horizontal_counter(); + /*! Gets the current latched horizontal counter; provided by the Sega VDPs only. */ + uint8_t get_latched_horizontal_counter() const; /*! Latches the current horizontal counter. */ void latch_horizontal_counter(); @@ -81,7 +109,7 @@ class TMS9918: public Base { If get_interrupt_line is true now of if get_interrupt_line would never return true, returns HalfCycles::max(). */ - HalfCycles get_next_sequence_point(); + HalfCycles get_next_sequence_point() const; /*! Returns the amount of time until the nominated line interrupt position is @@ -96,7 +124,7 @@ class TMS9918: public Base { /*! @returns @c true if the interrupt line is currently active; @c false otherwise. */ - bool get_interrupt_line(); + bool get_interrupt_line() const; }; } diff --git a/Components/9918/Implementation/9918.cpp b/Components/9918/Implementation/9918.cpp new file mode 100644 index 000000000..ff05d409e --- /dev/null +++ b/Components/9918/Implementation/9918.cpp @@ -0,0 +1,809 @@ +// +// 9918.cpp +// Clock Signal +// +// Created by Thomas Harte on 25/11/2017. +// Copyright 2017 Thomas Harte. All rights reserved. +// + +#include "../9918.hpp" + +#include +#include +#include +#include "../../../Outputs/Log.hpp" + +using namespace TI::TMS; + +namespace { + +// 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole +// line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368. +constexpr unsigned int CRTCyclesPerLine = 1365; +constexpr unsigned int CRTCyclesDivider = 4; + +} + +template +Base::Base() : + crt_(CRTCyclesPerLine, CRTCyclesDivider, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red8Green8Blue8) { + // Unimaginatively, this class just passes RGB through to the shader. Investigation is needed + // into whether there's a more natural form. It feels unlikely given the diversity of chips modelled. + + if constexpr (is_sega_vdp(personality)) { + mode_timing_.line_interrupt_position = 64; + + mode_timing_.end_of_frame_interrupt_position.column = 63; + mode_timing_.end_of_frame_interrupt_position.row = 193; + } + + // Establish that output is delayed after reading by `output_lag` cycles; start + // at a random position. + read_pointer_.row = rand() % 262; + read_pointer_.column = rand() % (Timing::CyclesPerLine - output_lag); + write_pointer_.row = read_pointer_.row; + write_pointer_.column = read_pointer_.column + output_lag; +} + +template +TMS9918::TMS9918() { + this->crt_.set_display_type(Outputs::Display::DisplayType::RGB); + this->crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f)); + + // The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement + // intended to produce the correct relationship between the hard edges between pixels and + // the colour clock. It was eyeballed rather than derived from any knowledge of the TMS + // colour burst generator because I've yet to find any. + this->crt_.set_immediate_default_phase(0.85f); +} + +template +void TMS9918::set_tv_standard(TVStandard standard) { + this->tv_standard_ = standard; + switch(standard) { + case TVStandard::PAL: + this->mode_timing_.total_lines = 313; + this->mode_timing_.first_vsync_line = 253; + this->crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::PAL50); + break; + default: + this->mode_timing_.total_lines = 262; + this->mode_timing_.first_vsync_line = 227; + this->crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::NTSC60); + break; + } +} + +template +void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) { + this->crt_.set_scan_target(scan_target); +} + +template +Outputs::Display::ScanStatus TMS9918::get_scaled_scan_status() const { + // The input was scaled by 3/4 to convert half cycles to internal ticks, + // so undo that and also allow for: (i) the multiply by 4 that it takes + // to reach the CRT; and (ii) the fact that the half-cycles value was scaled, + // and this should really reply in whole cycles. + return this->crt_.get_scaled_scan_status() * (4.0f / (3.0f * 8.0f)); +} + +template +void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) { + this->crt_.set_display_type(display_type); +} + +template +Outputs::Display::DisplayType TMS9918::get_display_type() const { + return this->crt_.get_display_type(); +} + +void LineBuffer::reset_sprite_collection() { + sprites_stopped = false; + active_sprite_slot = 0; + + for(int c = 0; c < 8; ++c) { + active_sprites[c].shift_position = 0; + } +} + +template +void Base::posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_position, int screen_row) { + if(!(status_ & StatusSpriteOverflow)) { + status_ = uint8_t((status_ & ~0x1f) | (sprite_number & 0x1f)); + } + if(buffer.sprites_stopped) return; + + // A sprite Y of 208 means "don't scan the list any further". + if(mode_timing_.allow_sprite_terminator && sprite_position == mode_timing_.sprite_terminator) { + buffer.sprites_stopped = true; + return; + } + + const int sprite_row = (((screen_row + 1) % mode_timing_.total_lines) - ((sprite_position + 1) & 255)) & 255; + if(sprite_row < 0 || sprite_row >= sprite_height_) return; + + if(buffer.active_sprite_slot == mode_timing_.maximum_visible_sprites) { + status_ |= StatusSpriteOverflow; + return; + } + + LineBuffer::ActiveSprite &sprite = buffer.active_sprites[buffer.active_sprite_slot]; + sprite.index = sprite_number; + sprite.row = sprite_row >> (sprites_magnified_ ? 1 : 0); + ++buffer.active_sprite_slot; +} + +template +void TMS9918::run_for(const HalfCycles cycles) { + // As specific as I've been able to get: + // Scanline time is always 228 cycles. + // PAL output is 313 lines total. NTSC output is 262 lines total. + // Interrupt is signalled upon entering the lower border. + + // Convert 456 clocked half cycles per line to 342 internal cycles per line; + // the internal clock is 1.5 times the nominal 3.579545 Mhz that I've advertised + // for this part. So multiply by three quarters. + const int int_cycles = this->clock_converter_.to_internal(cycles.as()); + if(!int_cycles) return; + + // 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) { +#ifndef NDEBUG + LineBufferPointer backup = this->read_pointer_; +#endif + + if(write_cycles_pool) { + // Determine how much writing to do. + const int write_cycles = std::min( + Timing::CyclesPerLine - this->write_pointer_.column, + write_cycles_pool + ); + const int end_column = this->write_pointer_.column + write_cycles; + LineBuffer &line_buffer = this->line_buffers_[this->write_pointer_.row]; + + // Determine what this does to any enqueued VRAM access. + this->minimum_access_column_ = this->write_pointer_.column + this->cycles_until_access_; + this->cycles_until_access_ -= write_cycles; + + + // --------------------------------------- + // Latch scrolling position, if necessary. + // --------------------------------------- + if constexpr (is_sega_vdp(personality)) { + if(this->write_pointer_.column < 61 && end_column >= 61) { + if(!this->write_pointer_.row) { + this->master_system_.latched_vertical_scroll = this->master_system_.vertical_scroll; + + if(this->master_system_.mode4_enable) { + this->mode_timing_.pixel_lines = 192; + if(this->mode2_enable_ && this->mode1_enable_) this->mode_timing_.pixel_lines = 224; + if(this->mode2_enable_ && this->mode3_enable_) this->mode_timing_.pixel_lines = 240; + + this->mode_timing_.allow_sprite_terminator = this->mode_timing_.pixel_lines == 192; + this->mode_timing_.first_vsync_line = (this->mode_timing_.total_lines + this->mode_timing_.pixel_lines) >> 1; + + this->mode_timing_.end_of_frame_interrupt_position.row = this->mode_timing_.pixel_lines + 1; + } + } + line_buffer.latched_horizontal_scroll = this->master_system_.horizontal_scroll; + } + } + + + + // ------------------------ + // Perform memory accesses. + // ------------------------ +#define fetch(function, clock) \ + const int first_window = from_internal(this->write_pointer_.column);\ + const int final_window = from_internal(end_column); \ + if(first_window == final_window) break; \ + if(final_window != clock_rate()) { \ + function(first_window, final_window); \ + } else { \ + function(first_window, final_window); \ + } + + switch(line_buffer.line_mode) { + case LineMode::Text: { fetch(this->template fetch_tms_text, Clock::TMSMemoryWindow); } break; + case LineMode::Character: { fetch(this->template fetch_tms_character, Clock::TMSMemoryWindow); } break; + case LineMode::SMS: { fetch(this->template fetch_sms, Clock::TMSMemoryWindow); } break; + case LineMode::Refresh: { fetch(this->template fetch_tms_refresh, Clock::TMSMemoryWindow); } break; + } + +#undef fetch + + + + // ------------------------------- + // Check for interrupt conditions. + // ------------------------------- + if(this->write_pointer_.column < this->mode_timing_.line_interrupt_position && end_column >= this->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 constexpr (is_sega_vdp(personality)) { + if(this->write_pointer_.row >= 0 && this->write_pointer_.row <= this->mode_timing_.pixel_lines) { + --this->line_interrupt_counter; + if(this->line_interrupt_counter == 0xff) { + this->line_interrupt_pending_ = true; + this->line_interrupt_counter = this->line_interrupt_target; + } + } else { + this->line_interrupt_counter = this->line_interrupt_target; + } + } + + // TODO: the V9938 provides line interrupts from direct specification of the target line. + // So life is easy. + } + + if( + this->write_pointer_.row == this->mode_timing_.end_of_frame_interrupt_position.row && + this->write_pointer_.column < this->mode_timing_.end_of_frame_interrupt_position.column && + end_column >= this->mode_timing_.end_of_frame_interrupt_position.column + ) { + this->status_ |= StatusInterrupt; + } + + + + // ------------- + // Advance time. + // ------------- + this->write_pointer_.column = end_column; + write_cycles_pool -= write_cycles; + + if(this->write_pointer_.column == Timing::CyclesPerLine) { + this->write_pointer_.column = 0; + this->write_pointer_.row = (this->write_pointer_.row + 1) % this->mode_timing_.total_lines; + LineBuffer &next_line_buffer = this->line_buffers_[this->write_pointer_.row]; + + // Establish the current screen output mode, which will be captured as a + // line mode momentarily. + this->screen_mode_ = this->current_screen_mode(); + + // Based on the output mode, pick a line mode. + next_line_buffer.first_pixel_output_column = Timing::FirstPixelCycle; + next_line_buffer.next_border_column = Timing::CyclesPerLine; + next_line_buffer.pixel_count = 256; + this->mode_timing_.maximum_visible_sprites = 4; + switch(this->screen_mode_) { + case ScreenMode::Text: + next_line_buffer.line_mode = LineMode::Text; + next_line_buffer.first_pixel_output_column = Timing::FirstTextCycle; + next_line_buffer.next_border_column = Timing::LastTextCycle; + next_line_buffer.pixel_count = 240; + break; + case ScreenMode::SMSMode4: + next_line_buffer.line_mode = LineMode::SMS; + this->mode_timing_.maximum_visible_sprites = 8; + break; + default: + next_line_buffer.line_mode = LineMode::Character; + break; + } + + if( + (this->screen_mode_ == ScreenMode::Blank) || + (this->write_pointer_.row >= this->mode_timing_.pixel_lines && this->write_pointer_.row != this->mode_timing_.total_lines-1)) + next_line_buffer.line_mode = LineMode::Refresh; + } + } + + +#ifndef NDEBUG + assert(backup.row == this->read_pointer_.row && backup.column == this->read_pointer_.column); + backup = this->write_pointer_; +#endif + + + if(read_cycles_pool) { + // Determine how much time has passed in the remainder of this line, and proceed. + const int target_read_cycles = std::min( + Timing::CyclesPerLine - this->read_pointer_.column, + read_cycles_pool + ); + int read_cycles_performed = 0; + uint32_t next_cram_value = 0; + + while(read_cycles_performed < target_read_cycles) { + int read_cycles = target_read_cycles - read_cycles_performed; + if(!read_cycles) continue; + + // Grab the next CRAM dot value and schedule a break in output if applicable. + const uint32_t cram_value = next_cram_value; + if constexpr (is_sega_vdp(personality)) { + next_cram_value = 0; + + if(!this->upcoming_cram_dots_.empty() && this->upcoming_cram_dots_.front().location.row == this->read_pointer_.row) { + int time_until_dot = this->upcoming_cram_dots_.front().location.column - this->read_pointer_.column; + + if(time_until_dot < read_cycles) { + read_cycles = time_until_dot; + next_cram_value = this->upcoming_cram_dots_.front().value; + this->upcoming_cram_dots_.erase(this->upcoming_cram_dots_.begin()); + } + } + } + + read_cycles_performed += read_cycles; + + const int end_column = this->read_pointer_.column + read_cycles; + LineBuffer &line_buffer = this->line_buffers_[this->read_pointer_.row]; + + + // -------------------- + // Output video stream. + // -------------------- + +#define crt_convert(action, time) this->crt_.action(from_internal(time)) +#define output_sync(x) crt_convert(output_sync, x) +#define output_blank(x) crt_convert(output_blank, x) +#define output_default_colour_burst(x) crt_convert(output_default_colour_burst, x) + +#define intersect(left, right, code) { \ + const int start = std::max(this->read_pointer_.column, left); \ + const int end = std::min(end_column, right); \ + if(end > start) {\ + code;\ + }\ + } + +#define border(left, right) intersect(left, right, this->output_border(end - start, cram_value)) + + if(line_buffer.line_mode == LineMode::Refresh || this->read_pointer_.row > this->mode_timing_.pixel_lines) { + if( + this->read_pointer_.row >= this->mode_timing_.first_vsync_line && + this->read_pointer_.row < this->mode_timing_.first_vsync_line + 4 + ) { + // Vertical sync. + // TODO: the Mega Drive supports interlaced video, I think? + if(end_column == Timing::CyclesPerLine) { + output_sync(Timing::CyclesPerLine); + } + } else { + // Right border. + border(0, Timing::EndOfRightBorder); + + // Blanking region: output the entire sequence when the cursor + // crosses the start-of-border point. + if( + this->read_pointer_.column < Timing::StartOfLeftBorder && + end_column >= Timing::StartOfLeftBorder + ) { + output_blank(Timing::StartOfSync - Timing::EndOfRightBorder); + output_sync(Timing::EndOfSync - Timing::StartOfSync); + output_blank(Timing::StartOfColourBurst - Timing::EndOfSync); + output_default_colour_burst(Timing::EndOfColourBurst - Timing::StartOfColourBurst); + output_blank(Timing::StartOfLeftBorder - Timing::EndOfColourBurst); + } + + // Border colour for the rest of the line. + border(Timing::StartOfLeftBorder, Timing::CyclesPerLine); + } + } else { + // Right border. + border(0, Timing::EndOfRightBorder); + + // Blanking region. + if( + this->read_pointer_.column < Timing::StartOfLeftBorder && + end_column >= Timing::StartOfLeftBorder + ) { + output_blank(Timing::StartOfSync - Timing::EndOfRightBorder); + output_sync(Timing::EndOfSync - Timing::StartOfSync); + output_blank(Timing::StartOfColourBurst - Timing::EndOfSync); + output_default_colour_burst(Timing::EndOfColourBurst - Timing::StartOfColourBurst); + output_blank(Timing::StartOfLeftBorder - Timing::EndOfColourBurst); + } + + // Left border. + border(Timing::StartOfLeftBorder, line_buffer.first_pixel_output_column); + +#define draw(function, clock) { \ + const int relative_start = from_internal(start - line_buffer.first_pixel_output_column); \ + const int relative_end = from_internal(end - line_buffer.first_pixel_output_column); \ + if(relative_start == relative_end) break; \ + this->function; } + + // Pixel region. + intersect( + line_buffer.first_pixel_output_column, + line_buffer.next_border_column, + if(!this->asked_for_write_area_) { + this->asked_for_write_area_ = true; + + this->pixel_origin_ = this->pixel_target_ = reinterpret_cast( + this->crt_.begin_data(line_buffer.pixel_count) + ); + } + + if(this->pixel_target_) { + switch(line_buffer.line_mode) { + case LineMode::SMS: draw(draw_sms(relative_start, relative_end, cram_value), Clock::TMSPixel); break; + case LineMode::Character: draw(draw_tms_character(relative_start, relative_end), Clock::TMSPixel); break; + case LineMode::Text: draw(draw_tms_text(relative_start, relative_end), Clock::TMSPixel); break; + + case LineMode::Refresh: break; /* Dealt with elsewhere. */ + } + } + + if(end == line_buffer.next_border_column) { + const int length = line_buffer.next_border_column - line_buffer.first_pixel_output_column; + this->crt_.output_data(from_internal(length), line_buffer.pixel_count); + this->pixel_origin_ = this->pixel_target_ = nullptr; + this->asked_for_write_area_ = false; + } + ); + +#undef draw + + // Additional right border, if called for. + if(line_buffer.next_border_column != Timing::CyclesPerLine) { + border(line_buffer.next_border_column, Timing::CyclesPerLine); + } + } + +#undef border +#undef intersect + +#undef crt_convert +#undef output_sync +#undef output_blank +#undef output_default_colour_burst + + + + // ------------- + // Advance time. + // ------------- + this->read_pointer_.column = end_column; + } + + read_cycles_pool -= target_read_cycles; + if(this->read_pointer_.column == Timing::CyclesPerLine) { + this->read_pointer_.column = 0; + this->read_pointer_.row = (this->read_pointer_.row + 1) % this->mode_timing_.total_lines; + } + } + + assert(backup.row == this->write_pointer_.row && backup.column == this->write_pointer_.column); + } +} + +template +void Base::output_border(int cycles, [[maybe_unused]] uint32_t cram_dot) { + cycles = from_internal(cycles); + const uint32_t border_colour = + is_sega_vdp(personality) ? + master_system_.colour_ram[16 + background_colour_] : + palette[background_colour_]; + + if constexpr (is_sega_vdp(personality)) { + if(cram_dot) { + uint32_t *const pixel_target = reinterpret_cast(crt_.begin_data(1)); + if(pixel_target) { + *pixel_target = border_colour | cram_dot; + } + + // Four CRT cycles is one pixel width, so this doesn't need clock conversion. + // TODO: on the Mega Drive it may be only 3 colour cycles, depending on mode. + crt_.output_level(4); + cycles -= 4; + } + } + + if(!cycles) { + return; + } + + // If the border colour is 0, that can be communicated + // more efficiently as an explicit blank. + if(border_colour) { + uint32_t *const pixel_target = reinterpret_cast(crt_.begin_data(1)); + if(pixel_target) { + *pixel_target = border_colour; + } + crt_.output_level(cycles); + } else { + crt_.output_blank(cycles); + } +} + +template +void TMS9918::write(int address, uint8_t value) { + // Writes to address 0 are writes to the video RAM. Store + // the value and return. + if(!(address & 1)) { + this->write_phase_ = false; + + // Enqueue the write to occur at the next available slot. + this->read_ahead_buffer_ = value; + this->queued_access_ = MemoryAccess::Write; + this->cycles_until_access_ = Timing::VRAMAccessDelay; + + return; + } + + // Writes to address 1 are performed in pairs; if this is the + // low byte of a value, store it and wait for the high byte. + if(!this->write_phase_) { + this->low_write_ = value; + this->write_phase_ = true; + + // The initial write should half update the access pointer. + this->ram_pointer_ = (this->ram_pointer_ & 0xff00) | this->low_write_; + return; + } + + // The RAM pointer is always set on a second write, regardless of + // whether the caller is intending to enqueue a VDP operation. + this->ram_pointer_ = (this->ram_pointer_ & 0x00ff) | uint16_t(value << 8); + + this->write_phase_ = false; + if(value & 0x80) { + if constexpr (is_sega_vdp(personality)) { + if(value & 0x40) { + this->master_system_.cram_is_selected = true; + return; + } + value &= 0xf; + } else { + value &= 0x7; + } + + // This is a write to a register. + switch(value) { + case 0: + if constexpr (is_sega_vdp(personality)) { + this->master_system_.vertical_scroll_lock = this->low_write_ & 0x80; + this->master_system_.horizontal_scroll_lock = this->low_write_ & 0x40; + this->master_system_.hide_left_column = this->low_write_ & 0x20; + this->enable_line_interrupts_ = this->low_write_ & 0x10; + this->master_system_.shift_sprites_8px_left = this->low_write_ & 0x08; + this->master_system_.mode4_enable = this->low_write_ & 0x04; + } + this->mode2_enable_ = this->low_write_ & 0x02; + break; + + case 1: + this->blank_display_ = !(this->low_write_ & 0x40); + this->generate_interrupts_ = this->low_write_ & 0x20; + this->mode1_enable_ = this->low_write_ & 0x10; + this->mode3_enable_ = this->low_write_ & 0x08; + this->sprites_16x16_ = this->low_write_ & 0x02; + this->sprites_magnified_ = this->low_write_ & 0x01; + + this->sprite_height_ = 8; + if(this->sprites_16x16_) this->sprite_height_ <<= 1; + if(this->sprites_magnified_) this->sprite_height_ <<= 1; + break; + + case 2: + this->pattern_name_address_ = size_t((this->low_write_ & 0xf) << 10) | 0x3ff; + this->master_system_.pattern_name_address = this->pattern_name_address_ | ((personality == TMS::SMSVDP) ? 0x000 : 0x400); + break; + + case 3: + this->colour_table_address_ = size_t(this->low_write_ << 6) | 0x3f; + break; + + case 4: + this->pattern_generator_table_address_ = size_t((this->low_write_ & 0x07) << 11) | 0x7ff; + break; + + case 5: + this->sprite_attribute_table_address_ = size_t((this->low_write_ & 0x7f) << 7) | 0x7f; + this->master_system_.sprite_attribute_table_address = this->sprite_attribute_table_address_ | ((personality == TMS::SMSVDP) ? 0x00 : 0x80); + break; + + case 6: + this->sprite_generator_table_address_ = size_t((this->low_write_ & 0x07) << 11) | 0x7ff; + this->master_system_.sprite_generator_table_address = this->sprite_generator_table_address_ | ((personality == TMS::SMSVDP) ? 0x0000 : 0x1800); + break; + + case 7: + this->text_colour_ = this->low_write_ >> 4; + this->background_colour_ = this->low_write_ & 0xf; + break; + + case 8: + if constexpr (is_sega_vdp(personality)) { + this->master_system_.horizontal_scroll = this->low_write_; + } + break; + + case 9: + if constexpr (is_sega_vdp(personality)) { + this->master_system_.vertical_scroll = this->low_write_; + } + break; + + case 10: + if constexpr (is_sega_vdp(personality)) { + this->line_interrupt_target = this->low_write_; + } + break; + + default: + LOG("Unknown TMS write: " << int(this->low_write_) << " to " << int(value)); + break; + } + } else { + // This is an access via the RAM pointer. + if(!(value & 0x40)) { + // A read request is enqueued upon setting the address; conversely a write + // won't be enqueued unless and until some actual data is supplied. + this->queued_access_ = MemoryAccess::Read; + this->cycles_until_access_ = Timing::VRAMAccessDelay; + } + this->master_system_.cram_is_selected = false; + } +} + +template +uint8_t TMS9918::get_current_line() const { + // Determine the row to return. + constexpr int row_change_position = 63; // This is the proper Master System value; substitute if any other VDPs turn out to have this functionality. + int source_row = + (this->write_pointer_.column < row_change_position) + ? (this->write_pointer_.row + this->mode_timing_.total_lines - 1) % this->mode_timing_.total_lines + : this->write_pointer_.row; + + if(this->tv_standard_ == TVStandard::NTSC) { + if(this->mode_timing_.pixel_lines == 240) { + // NTSC 256x240: 00-FF, 00-06 + } else if(this->mode_timing_.pixel_lines == 224) { + // NTSC 256x224: 00-EA, E5-FF + if(source_row >= 0xeb) source_row -= 6; + } else { + // NTSC 256x192: 00-DA, D5-FF + if(source_row >= 0xdb) source_row -= 6; + } + } else { + if(this->mode_timing_.pixel_lines == 240) { + // PAL 256x240: 00-FF, 00-0A, D2-FF + if(source_row >= 267) source_row -= 0x39; + } else if(this->mode_timing_.pixel_lines == 224) { + // PAL 256x224: 00-FF, 00-02, CA-FF + if(source_row >= 259) source_row -= 0x39; + } else { + // PAL 256x192: 00-F2, BA-FF + if(source_row >= 0xf3) source_row -= 0x39; + } + } + + return uint8_t(source_row); +} + +template +uint8_t TMS9918::read(int address) { + this->write_phase_ = false; + + // Reads from address 0 read video RAM, via the read-ahead buffer. + if(!(address & 1)) { + // Enqueue the write to occur at the next available slot. + const uint8_t result = this->read_ahead_buffer_; + this->queued_access_ = MemoryAccess::Read; + return result; + } + + // Reads from address 1 get the status register. + const uint8_t result = this->status_; + this->status_ &= ~(StatusInterrupt | StatusSpriteOverflow | StatusSpriteCollision); + this->line_interrupt_pending_ = false; + return result; +} + +template +HalfCycles TMS9918::get_next_sequence_point() const { + if(!this->generate_interrupts_ && !this->enable_line_interrupts_) return HalfCycles::max(); + if(get_interrupt_line()) return HalfCycles::max(); + + // Calculate the amount of time until the next end-of-frame interrupt. + const int frame_length = Timing::CyclesPerLine * this->mode_timing_.total_lines; + int time_until_frame_interrupt = + ( + ((this->mode_timing_.end_of_frame_interrupt_position.row * Timing::CyclesPerLine) + this->mode_timing_.end_of_frame_interrupt_position.column + frame_length) - + ((this->write_pointer_.row * Timing::CyclesPerLine) + this->write_pointer_.column) + ) % frame_length; + if(!time_until_frame_interrupt) time_until_frame_interrupt = frame_length; + + if(!this->enable_line_interrupts_) { + return this->clock_converter_.half_cycles_before_internal_cycles(time_until_frame_interrupt); + } + + // Calculate when the next line interrupt will occur. + int next_line_interrupt_row = -1; + + int cycles_to_next_interrupt_threshold = this->mode_timing_.line_interrupt_position - this->write_pointer_.column; + int line_of_next_interrupt_threshold = this->write_pointer_.row; + if(cycles_to_next_interrupt_threshold <= 0) { + cycles_to_next_interrupt_threshold += Timing::CyclesPerLine; + ++line_of_next_interrupt_threshold; + } + + if constexpr (is_sega_vdp(personality)) { + // 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(line_of_next_interrupt_threshold + this->line_interrupt_counter <= this->mode_timing_.pixel_lines) { + next_line_interrupt_row = line_of_next_interrupt_threshold + this->line_interrupt_counter; + } else { + if(this->line_interrupt_target <= this->mode_timing_.pixel_lines) + next_line_interrupt_row = this->mode_timing_.total_lines + this->line_interrupt_target; + } + } + + // If there's actually no interrupt upcoming, despite being enabled, either return + // the frame end interrupt or no interrupt pending as appropriate. + if(next_line_interrupt_row == -1) { + return this->generate_interrupts_ ? + this->clock_converter_.half_cycles_before_internal_cycles(time_until_frame_interrupt) : + HalfCycles::max(); + } + + // 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. + const int local_cycles_until_line_interrupt = cycles_to_next_interrupt_threshold + (next_line_interrupt_row - line_of_next_interrupt_threshold) * Timing::CyclesPerLine; + if(!this->generate_interrupts_) return this->clock_converter_.half_cycles_before_internal_cycles(local_cycles_until_line_interrupt); + + // Return whichever interrupt is closer. + return this->clock_converter_.half_cycles_before_internal_cycles(std::min(local_cycles_until_line_interrupt, time_until_frame_interrupt)); +} + +template +HalfCycles TMS9918::get_time_until_line(int line) { + if(line < 0) line += this->mode_timing_.total_lines; + + int cycles_to_next_interrupt_threshold = this->mode_timing_.line_interrupt_position - this->write_pointer_.column; + int line_of_next_interrupt_threshold = this->write_pointer_.row; + if(cycles_to_next_interrupt_threshold <= 0) { + cycles_to_next_interrupt_threshold += Timing::CyclesPerLine; + ++line_of_next_interrupt_threshold; + } + + if(line_of_next_interrupt_threshold > line) { + line += this->mode_timing_.total_lines; + } + + return this->clock_converter_.half_cycles_before_internal_cycles(cycles_to_next_interrupt_threshold + (line - line_of_next_interrupt_threshold)*Timing::CyclesPerLine); +} + +template +bool TMS9918::get_interrupt_line() const { + return + ((this->status_ & StatusInterrupt) && this->generate_interrupts_) || + (this->enable_line_interrupts_ && this->line_interrupt_pending_); +} + +// TODO: [potentially] remove Master System timing assumptions in latch and get_latched below. +template uint8_t TMS9918::get_latched_horizontal_counter() const { + // Translate from internal numbering, which puts pixel output + // in the final 256 pixels of 342, to the public numbering, + // which counts the 256 pixels as items 0–255, starts + // counting at -48, and returns only the top 8 bits of the number. + int public_counter = this->latched_column_ - (342 - 256); + if(public_counter < -46) public_counter += 342; + return uint8_t(public_counter >> 1); +} + +template +void TMS9918::latch_horizontal_counter() { + this->latched_column_ = this->write_pointer_.column; +} + +template class TI::TMS::TMS9918; +template class TI::TMS::TMS9918; +template class TI::TMS::TMS9918; +template class TI::TMS::TMS9918; +template class TI::TMS::TMS9918; +template class TI::TMS::TMS9918; +template class TI::TMS::TMS9918; diff --git a/Components/9918/Implementation/9918Base.hpp b/Components/9918/Implementation/9918Base.hpp index c0bb8c86d..70a31ea30 100644 --- a/Components/9918/Implementation/9918Base.hpp +++ b/Components/9918/Implementation/9918Base.hpp @@ -9,346 +9,326 @@ #ifndef TMS9918Base_hpp #define TMS9918Base_hpp -#include "../../../Outputs/CRT/CRT.hpp" -#include "../../../ClockReceiver/ClockReceiver.hpp" +#include "ClockConverter.hpp" +#include "../../../ClockReceiver/ClockReceiver.hpp" +#include "../../../Numeric/BitReverse.hpp" +#include "../../../Outputs/CRT/CRT.hpp" + +#include "PersonalityTraits.hpp" + +#include #include #include +#include #include #include namespace TI { namespace TMS { -enum Personality { - TMS9918A, // includes the 9928 and 9929; set TV standard and output device as desired. - V9938, - V9958, - SMSVDP, - SMS2VDP, - GGVDP, +// 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 }; -enum class TVStandard { - /*! i.e. 50Hz output at around 312.5 lines/field */ - PAL, - /*! i.e. 60Hz output at around 262.5 lines/field */ - NTSC +enum class LineMode { + Text, + Character, + Refresh, + SMS }; -#define is_sega_vdp(x) ((x) >= SMSVDP) +enum class MemoryAccess { + Read, Write, None +}; -class Base { - public: - static uint32_t palette_pack(uint8_t r, uint8_t g, uint8_t b) { - uint32_t result = 0; - uint8_t *const result_ptr = reinterpret_cast(&result); - result_ptr[0] = r; - result_ptr[1] = g; - result_ptr[2] = b; - result_ptr[3] = 0; - return result; - } +// Temporary buffers collect a representation of each line prior to pixel serialisation. +struct LineBuffer { + // The line mode describes the proper timing diagram for this line. + LineMode line_mode = LineMode::Text; - protected: - static constexpr int output_lag = 11; // i.e. pixel output will occur 11 cycles after corresponding data read. + // 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 default TMS palette. - const uint32_t palette[16] = { - palette_pack(0, 0, 0), - palette_pack(0, 0, 0), - palette_pack(33, 200, 66), - palette_pack(94, 220, 120), + // The names array holds pattern names, as an offset into memory, and + // potentially flags also. + struct { + size_t offset = 0; + uint8_t flags = 0; + } names[40]; - palette_pack(84, 85, 237), - palette_pack(125, 118, 252), - palette_pack(212, 82, 77), - palette_pack(66, 235, 245), + // 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]; - palette_pack(252, 85, 84), - palette_pack(255, 121, 120), - palette_pack(212, 193, 84), - palette_pack(230, 206, 128), + /* + Horizontal layout (on a 342-cycle clock): - palette_pack(33, 176, 59), - palette_pack(201, 91, 186), - palette_pack(204, 204, 204), - palette_pack(255, 255, 255) - }; + 15 cycles right border + 58 cycles blanking & sync + 13 cycles left border - Base(Personality p); + ... i.e. to cycle 86, then: - const Personality personality_; - Outputs::CRT::CRT crt_; - TVStandard tv_standard_ = TVStandard::NTSC; + border up to first_pixel_output_column; + pixels up to next_border_column; + border up to the end. - // Holds the contents of this VDP's connected DRAM. - std::vector ram_; + e.g. standard 256-pixel modes will want to set + first_pixel_output_column = 86, next_border_column = 342. + */ + int first_pixel_output_column = 94; + int next_border_column = 334; + size_t pixel_count = 256; - // Holds the state of the DRAM/CRAM-access mechanism. - uint16_t ram_pointer_ = 0; - uint8_t read_ahead_buffer_ = 0; - enum class MemoryAccess { - Read, Write, None - } queued_access_ = MemoryAccess::None; - int cycles_until_access_ = 0; - int minimum_access_column_ = 0; - int vram_access_delay() { - // This seems to be correct for all currently-modelled VDPs; - // it's the delay between an external device scheduling a - // read or write and the very first time that can occur - // (though, in practice, it won't happen until the next - // external slot after this number of cycles after the - // device has requested the read or write). - return 6; - } + // 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. - // Holds the main status register. - uint8_t status_ = 0; + 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]; - // Current state of programmer input. - bool write_phase_ = false; // Determines whether the VDP is expecting the low or high byte of a write. - uint8_t low_write_ = 0; // Buffers the low byte of a write. + 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. - // Various programmable flags. - bool mode1_enable_ = false; - bool mode2_enable_ = false; - bool mode3_enable_ = false; - bool blank_display_ = false; - bool sprites_16x16_ = false; - bool sprites_magnified_ = false; - bool generate_interrupts_ = false; - int sprite_height_ = 8; + void reset_sprite_collection(); +}; - 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. +struct LineBufferPointer { + int row, column; +}; - uint8_t text_colour_ = 0; - uint8_t background_colour_ = 0; +constexpr uint8_t StatusInterrupt = 0x80; +constexpr uint8_t StatusSpriteOverflow = 0x40; - // 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); +constexpr int StatusSpriteCollisionShift = 5; +constexpr uint8_t StatusSpriteCollision = 0x20; - // Internal mechanisms for position tracking. - int latched_column_ = 0; +template struct Base { + Base(); - // A helper function to output the current border colour for - // the number of cycles supplied. - void output_border(int cycles, uint32_t cram_dot); + static constexpr int output_lag = 11; // i.e. pixel output will occur 11 cycles + // after corresponding data read. - // A struct to contain timing information for the current mode. + static constexpr uint32_t palette_pack(uint8_t r, uint8_t g, uint8_t b) { + #if TARGET_RT_BIG_ENDIAN + return uint32_t((r << 24) | (g << 16) | (b << 8)); + #else + return uint32_t((b << 16) | (g << 8) | r); + #endif + } + + // The default TMS palette. + static constexpr std::array palette { + palette_pack(0, 0, 0), + palette_pack(0, 0, 0), + palette_pack(33, 200, 66), + palette_pack(94, 220, 120), + + palette_pack(84, 85, 237), + palette_pack(125, 118, 252), + palette_pack(212, 82, 77), + palette_pack(66, 235, 245), + + palette_pack(252, 85, 84), + palette_pack(255, 121, 120), + palette_pack(212, 193, 84), + palette_pack(230, 206, 128), + + palette_pack(33, 176, 59), + palette_pack(201, 91, 186), + palette_pack(204, 204, 204), + palette_pack(255, 255, 255) + }; + + Outputs::CRT::CRT crt_; + TVStandard tv_standard_ = TVStandard::NTSC; + + // Personality-specific metrics and converters. + ClockConverter clock_converter_; + + // This VDP's DRAM. + std::array ram_; + + // State of the DRAM/CRAM-access mechanism. + uint16_t ram_pointer_ = 0; + uint8_t read_ahead_buffer_ = 0; + MemoryAccess queued_access_ = MemoryAccess::None; + int cycles_until_access_ = 0; + int minimum_access_column_ = 0; + + // The main status register. + uint8_t status_ = 0; + + // Current state of programmer input. + bool write_phase_ = false; // Determines whether the VDP is expecting the low or high byte of a write. + uint8_t low_write_ = 0; // Buffers the low byte of a write. + + // Various programmable flags. + bool mode1_enable_ = false; + bool mode2_enable_ = false; + bool mode3_enable_ = false; + bool blank_display_ = false; + bool sprites_16x16_ = false; + bool sprites_magnified_ = false; + bool generate_interrupts_ = false; + int sprite_height_ = 8; + + // Programmer-specified addresses. + 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. + + // Default colours. + uint8_t text_colour_ = 0; + uint8_t background_colour_ = 0; + + // Internal mechanisms for position tracking. + int latched_column_ = 0; + + // A struct to contain timing information that is a function of the current mode. + struct { + /* + Vertical layout: + + Lines 0 to [pixel_lines]: standard data fetch and drawing will occur. + ... to [first_vsync_line]: refresh fetches will occur and border will be output. + .. to [2.5 or 3 lines later]: vertical sync is output. + ... to [total lines - 1]: refresh fetches will occur and border will be output. + ... for one line: standard data fetch will occur, without drawing. + */ + int total_lines = 262; + int pixel_lines = 192; + int first_vsync_line = 227; + + // Maximum number of sprite slots to populate; + // if sprites beyond this number should be visible + // then the appropriate status information will be set. + int maximum_visible_sprites = 4; + + // Set the position, in cycles, of the two interrupts, + // within a line. struct { - /* - Vertical layout: + int column = 4; + int row = 193; + } end_of_frame_interrupt_position; + int line_interrupt_position = -1; - Lines 0 to [pixel_lines]: standard data fetch and drawing will occur. - ... to [first_vsync_line]: refresh fetches will occur and border will be output. - .. to [2.5 or 3 lines later]: vertical sync is output. - ... to [total lines - 1]: refresh fetches will occur and border will be output. - ... for one line: standard data fetch will occur, without drawing. - */ - int total_lines = 262; - int pixel_lines = 192; - int first_vsync_line = 227; + // Enables or disabled the recognition of the sprite + // list terminator, and sets the terminator value. + bool allow_sprite_terminator = true; + uint8_t sprite_terminator = 0xd0; + } mode_timing_; - // Maximum number of sprite slots to populate; - // if sprites beyond this number should be visible - // then the appropriate status information will be set. - int maximum_visible_sprites = 4; + uint8_t line_interrupt_target = 0xff; + uint8_t line_interrupt_counter = 0; + bool enable_line_interrupts_ = false; + bool line_interrupt_pending_ = false; - // Set the position, in cycles, of the two interrupts, - // within a line. - struct { - int column = 4; - int row = 193; - } end_of_frame_interrupt_position; - int line_interrupt_position = -1; + ScreenMode screen_mode_; + LineBuffer line_buffers_[313]; + void posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_y, int screen_row); - // Enables or disabled the recognition of the sprite - // list terminator, and sets the terminator value. - bool allow_sprite_terminator = true; - uint8_t sprite_terminator = 0xd0; - } mode_timing_; + // 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. + LineBufferPointer read_pointer_, write_pointer_; - uint8_t line_interrupt_target = 0xff; - uint8_t line_interrupt_counter = 0; - bool enable_line_interrupts_ = false; - bool line_interrupt_pending_ = false; + // The SMS VDP has a programmer-set colour palette, with a dedicated patch of RAM. But the RAM is only exactly + // fast enough for the pixel clock. So when the programmer writes to it, that causes a one-pixel glitch; there + // isn't the bandwidth for the read both write to occur simultaneously. The following buffer therefore keeps + // track of pending collisions, for visual reproduction. + struct CRAMDot { + LineBufferPointer location; + uint32_t value; + }; + std::vector upcoming_cram_dots_; - // 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_; + // Extra information that affects the Master System output mode. + struct { + // Programmer-set flags. + bool vertical_scroll_lock = false; + bool horizontal_scroll_lock = false; + bool hide_left_column = false; + bool shift_sprites_8px_left = false; + bool mode4_enable = false; + uint8_t horizontal_scroll = 0; + uint8_t vertical_scroll = 0; - enum class LineMode { - Text, - Character, - Refresh, - SMS - }; + // The Master System's additional colour RAM. + uint32_t colour_ram[32]; + bool cram_is_selected = false; - // Temporary buffers collect a representation of this line prior to pixel serialisation. - struct LineBuffer { - // The line mode describes the proper timing diagram for this line. - LineMode line_mode = LineMode::Text; + // 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; - // 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; + size_t pattern_name_address; + size_t sprite_attribute_table_address; + size_t sprite_generator_table_address; + } master_system_; - // The names array holds pattern names, as an offset into memory, and - // potentially flags also. - struct { - size_t offset = 0; - uint8_t flags = 0; - } 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 = 94; - int next_border_column = 334; - - // 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_[313]; - 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_; - - // The SMS VDP has a programmer-set colour palette, with a dedicated patch of RAM. But the RAM is only exactly - // fast enough for the pixel clock. So when the programmer writes to it, that causes a one-pixel glitch; there - // isn't the bandwidth for the read both write to occur simultaneously. The following buffer therefore keeps - // track of pending collisions, for visual reproduction. - struct CRAMDot { - LineBufferPointer location; - uint32_t value; - }; - std::vector upcoming_cram_dots_; - - // Extra information that affects the Master System output mode. - struct { - // Programmer-set flags. - bool vertical_scroll_lock = false; - bool horizontal_scroll_lock = false; - bool hide_left_column = false; - bool shift_sprites_8px_left = false; - bool mode4_enable = false; - uint8_t horizontal_scroll = 0; - uint8_t vertical_scroll = 0; - - // The Master System's additional colour RAM. - uint32_t colour_ram[32]; - bool cram_is_selected = false; - - // 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; - - size_t pattern_name_address; - size_t sprite_attribute_table_address; - size_t sprite_generator_table_address; - } master_system_; - - void set_current_screen_mode() { - if(blank_display_) { - screen_mode_ = ScreenMode::Blank; - return; - } - - if(is_sega_vdp(personality_) && master_system_.mode4_enable) { - screen_mode_ = ScreenMode::SMSMode4; - mode_timing_.maximum_visible_sprites = 8; - return; - } - - mode_timing_.maximum_visible_sprites = 4; - if(!mode1_enable_ && !mode2_enable_ && !mode3_enable_) { - screen_mode_ = ScreenMode::ColouredText; - return; - } - - if(mode1_enable_ && !mode2_enable_ && !mode3_enable_) { - screen_mode_ = ScreenMode::Text; - return; - } - - if(!mode1_enable_ && mode2_enable_ && !mode3_enable_) { - screen_mode_ = ScreenMode::Graphics; - return; - } - - if(!mode1_enable_ && !mode2_enable_ && mode3_enable_) { - screen_mode_ = ScreenMode::MultiColour; - return; - } - - // TODO: undocumented TMS modes. - screen_mode_ = ScreenMode::Blank; + ScreenMode current_screen_mode() const { + if(blank_display_) { + return ScreenMode::Blank; } - void do_external_slot(int access_column) { - // Don't do anything if the required time for the access to become executable - // has yet to pass. - if(access_column < minimum_access_column_) { - return; + if constexpr (is_sega_vdp(personality)) { + if(master_system_.mode4_enable) { + return ScreenMode::SMSMode4; } + } - switch(queued_access_) { - default: return; + if(!mode1_enable_ && !mode2_enable_ && !mode3_enable_) { + return ScreenMode::ColouredText; + } - case MemoryAccess::Write: + if(mode1_enable_ && !mode2_enable_ && !mode3_enable_) { + return ScreenMode::Text; + } + + if(!mode1_enable_ && mode2_enable_ && !mode3_enable_) { + return ScreenMode::Graphics; + } + + if(!mode1_enable_ && !mode2_enable_ && mode3_enable_) { + return ScreenMode::MultiColour; + } + + // TODO: undocumented TMS modes. + return ScreenMode::Blank; + } + + void do_external_slot(int access_column) { + // Don't do anything if the required time for the access to become executable + // has yet to pass. + if(access_column < minimum_access_column_) { + return; + } + + switch(queued_access_) { + default: return; + + case MemoryAccess::Write: + if constexpr (is_sega_vdp(personality)) { if(master_system_.cram_is_selected) { // Adjust the palette. In a Master System blue has a slightly different // scale; cf. https://www.retrorgb.com/sega-master-system-non-linear-blue-channel-findings.html @@ -363,9 +343,7 @@ class Base { // Schedule a CRAM dot; this is scheduled for wherever it should appear // on screen. So it's wherever the output stream would be now. Which // is output_lag cycles ago from the point of view of the input stream. - upcoming_cram_dots_.emplace_back(); - CRAMDot &dot = upcoming_cram_dots_.back(); - + CRAMDot &dot = upcoming_cram_dots_.emplace_back(); dot.location.column = write_pointer_.column - output_lag; dot.location.row = write_pointer_.row; @@ -379,484 +357,47 @@ class Base { dot.location.column %= 342; dot.value = master_system_.colour_ram[ram_pointer_ & 0x1f]; - } else { - ram_[ram_pointer_ & 16383] = read_ahead_buffer_; + break; } - break; - case MemoryAccess::Read: - read_ahead_buffer_ = ram_[ram_pointer_ & 16383]; - break; - } - ++ram_pointer_; - queued_access_ = MemoryAccess::None; + } + ram_[ram_pointer_ & 16383] = read_ahead_buffer_; + break; + case MemoryAccess::Read: + read_ahead_buffer_ = ram_[ram_pointer_ & 16383]; + break; } - -/* - Fetching routines follow below; they obey the following rules: - - 1) input is a start position and an end position; they should perform the proper - operations for the period: start <= time < end. - 2) times are measured relative to a 172-cycles-per-line clock (so: they directly - count access windows on the TMS and Master System). - 3) time 0 is the beginning of the access window immediately after the last pattern/data - block fetch that would contribute to this line, in a normal 32-column mode. So: - - * it's cycle 309 on Mattias' TMS diagram; - * it's cycle 1238 on his V9938 diagram; - * it's after the last background render block in Mask of Destiny's Master System timing diagram. - - That division point was selected, albeit arbitrarily, because it puts all the tile - fetches for a single line into the same [0, 171] period. - - 4) all of these functions are templated with a `use_end` parameter. That will be true if - end is < 172, false otherwise. So functions can use it to eliminate should-exit-not checks, - for the more usual path of execution. - - Provided for the benefit of the methods below: - - * the function external_slot(), which will perform any pending VRAM read/write. - * the macros slot(n) and external_slot(n) which can be used to schedule those things inside a - switch(start)-based implementation. - - All functions should just spool data to intermediary storage. This is because for most VDPs there is - a decoupling between fetch pattern and output pattern, and it's neater to keep the same division - for the exceptions. -*/ - -#define slot(n) \ - if(use_end && end == n) return; \ - [[fallthrough]]; \ - case n - -#define external_slot(n) \ - slot(n): do_external_slot((n)*2); - -#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) \ - slot(location): \ - external_slot(location+1); - -#define refreshes_2(location) \ - refresh(location); \ - refresh(location+2); - -#define refreshes_4(location) \ - refreshes_2(location); \ - refreshes_2(location+4); - -#define refreshes_8(location) \ - refreshes_4(location); \ - refreshes_4(location+8); - - switch(start) { - default: assert(false); - - /* 44 external slots */ - external_slots_32(0) - external_slots_8(32) - external_slots_4(40) - - /* 64 refresh/external slot pairs (= 128 windows) */ - refreshes_8(44); - refreshes_8(60); - refreshes_8(76); - refreshes_8(92); - refreshes_8(108); - refreshes_8(124); - refreshes_8(140); - refreshes_8(156); - - return; - } - -#undef refreshes_8 -#undef refreshes_4 -#undef refreshes_2 -#undef refresh - } - - template void fetch_tms_text(int start, int end) { -#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); \ - external_slot(location+1); \ - fetch_tile_pattern(location+2, column); - -#define fetch_columns_2(location, column) \ - fetch_column(location, column); \ - fetch_column(location+3, column+1); - -#define fetch_columns_4(location, column) \ - fetch_columns_2(location, column); \ - fetch_columns_2(location+6, column+2); - -#define fetch_columns_8(location, column) \ - fetch_columns_4(location, column); \ - fetch_columns_4(location+12, column+4); - - LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; - const size_t row_base = pattern_name_address_ & (0x3c00 | size_t(write_pointer_.row >> 3) * 40); - const size_t row_offset = pattern_generator_table_address_ & (0x3800 | (write_pointer_.row & 7)); - - switch(start) { - default: assert(false); - - /* 47 external slots (= 47 windows) */ - external_slots_32(0) - external_slots_8(32) - external_slots_4(40) - external_slots_2(44) - external_slot(46) - - /* 40 column fetches (= 120 windows) */ - fetch_columns_8(47, 0); - fetch_columns_8(71, 8); - fetch_columns_8(95, 16); - fetch_columns_8(119, 24); - fetch_columns_8(143, 32); - - /* 5 more external slots */ - external_slots_4(167); - external_slot(171); - - return; - } - -#undef fetch_columns_8 -#undef fetch_columns_4 -#undef fetch_columns_2 -#undef fetch_column -#undef fetch_tile_pattern -#undef fetch_tile_name - } - - template void fetch_tms_character(int start, int end) { -#define sprite_fetch_coordinates(location, sprite) \ - slot(location): \ - slot(location+1): \ - line_buffer.active_sprites[sprite].x = \ - ram_[\ - sprite_attribute_table_address_ & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 2))\ - ]; - - // This implementation doesn't refetch Y; it's unclear to me - // whether it's refetched. - -#define sprite_fetch_graphics(location, sprite) \ - slot(location): \ - slot(location+1): \ - slot(location+2): \ - slot(location+3): {\ - const uint8_t name = ram_[\ - sprite_attribute_table_address_ & size_t(0x3f82 | (line_buffer.active_sprites[sprite].index << 2))\ - ] & (sprites_16x16_ ? ~3 : ~0);\ - line_buffer.active_sprites[sprite].image[2] = ram_[\ - sprite_attribute_table_address_ & size_t(0x3f83 | (line_buffer.active_sprites[sprite].index << 2))\ - ];\ - line_buffer.active_sprites[sprite].x -= (line_buffer.active_sprites[sprite].image[2] & 0x80) >> 2;\ - const size_t graphic_location = sprite_generator_table_address_ & size_t(0x3800 | (name << 3) | line_buffer.active_sprites[sprite].row); \ - line_buffer.active_sprites[sprite].image[0] = ram_[graphic_location];\ - line_buffer.active_sprites[sprite].image[1] = ram_[graphic_location+16];\ + ++ram_pointer_; + queued_access_ = MemoryAccess::None; } -#define sprite_fetch_block(location, sprite) \ - sprite_fetch_coordinates(location, sprite) \ - sprite_fetch_graphics(location+2, sprite) + // Various fetchers. + template void fetch_tms_refresh(int start, int end); + template void fetch_tms_text(int start, int end); + template void fetch_tms_character(int start, int end); -#define sprite_y_read(location, sprite) \ - slot(location): posit_sprite(sprite_selection_buffer, sprite, ram_[sprite_attribute_table_address_ & (((sprite) << 2) | 0x3f80)], write_pointer_.row); + template void fetch_yamaha_refresh(int start, int end); + template void fetch_yamaha_no_sprites(int start, int end); + template void fetch_yamaha_sprites(int start, int end); -#define fetch_tile_name(column) line_buffer.names[column].offset = ram_[(row_base + column) & 0x3fff]; + template void fetch_sms(int start, int end); -#define fetch_tile(column) {\ - line_buffer.patterns[column][1] = ram_[(colour_base + size_t((line_buffer.names[column].offset << 3) >> colour_name_shift)) & 0x3fff]; \ - line_buffer.patterns[column][0] = ram_[(pattern_base + size_t(line_buffer.names[column].offset << 3)) & 0x3fff]; \ - } + // A helper function to output the current border colour for + // the number of cycles supplied. + void output_border(int cycles, uint32_t cram_dot); -#define background_fetch_block(location, column, sprite) \ - slot(location): fetch_tile_name(column) \ - external_slot(location+1); \ - slot(location+2): \ - slot(location+3): fetch_tile(column) \ - slot(location+4): fetch_tile_name(column+1) \ - sprite_y_read(location+5, sprite); \ - slot(location+6): \ - slot(location+7): fetch_tile(column+1) \ - slot(location+8): fetch_tile_name(column+2) \ - sprite_y_read(location+9, sprite+1); \ - slot(location+10): \ - slot(location+11): fetch_tile(column+2) \ - slot(location+12): fetch_tile_name(column+3) \ - sprite_y_read(location+13, sprite+2); \ - slot(location+14): \ - slot(location+15): fetch_tile(column+3) + // Output serialisation state. + uint32_t *pixel_target_ = nullptr, *pixel_origin_ = nullptr; + bool asked_for_write_area_ = false; - LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; - LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines]; - const size_t row_base = pattern_name_address_ & (size_t((write_pointer_.row << 2)&~31) | 0x3c00); - - size_t pattern_base = pattern_generator_table_address_; - size_t colour_base = colour_table_address_; - int colour_name_shift = 6; - - if(screen_mode_ == ScreenMode::Graphics) { - // If this is high resolution mode, allow the row number to affect the pattern and colour addresses. - pattern_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5)); - colour_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5)); - - colour_base += size_t(write_pointer_.row & 7); - colour_name_shift = 0; - } else { - colour_base &= size_t(0xffc0); - pattern_base &= size_t(0x3800); - } - - if(screen_mode_ == ScreenMode::MultiColour) { - pattern_base += size_t((write_pointer_.row >> 2) & 7); - } else { - pattern_base += size_t(write_pointer_.row & 7); - } - - switch(start) { - default: assert(false); - - external_slots_2(0); - - sprite_fetch_block(2, 0); - sprite_fetch_block(8, 1); - sprite_fetch_coordinates(14, 2); - - external_slots_4(16); - external_slot(20); - - sprite_fetch_graphics(21, 2); - sprite_fetch_block(25, 3); - - slot(31): - sprite_selection_buffer.reset_sprite_collection(); - do_external_slot(31*2); - external_slots_2(32); - external_slot(34); - - sprite_y_read(35, 0); - sprite_y_read(36, 1); - sprite_y_read(37, 2); - sprite_y_read(38, 3); - sprite_y_read(39, 4); - sprite_y_read(40, 5); - sprite_y_read(41, 6); - sprite_y_read(42, 7); - - background_fetch_block(43, 0, 8); - background_fetch_block(59, 4, 11); - background_fetch_block(75, 8, 14); - background_fetch_block(91, 12, 17); - background_fetch_block(107, 16, 20); - background_fetch_block(123, 20, 23); - background_fetch_block(139, 24, 26); - background_fetch_block(155, 28, 29); - - return; - } - -#undef background_fetch_block -#undef fetch_tile -#undef fetch_tile_name -#undef sprite_y_read -#undef sprite_fetch_block -#undef sprite_fetch_graphics -#undef sprite_fetch_coordinates - } - - -/*********************************************** - Master System Fetching Code -************************************************/ - - template void fetch_sms(int start, int end) { -#define sprite_fetch(sprite) {\ - line_buffer.active_sprites[sprite].x = \ - ram_[\ - master_system_.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_[\ - master_system_.sprite_attribute_table_address & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 1))\ - ] & (sprites_16x16_ ? ~1 : ~0);\ - const size_t graphic_location = master_system_.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) \ - slot(location): \ - slot(location+1): \ - slot(location+2): \ - slot(location+3): \ - slot(location+4): \ - slot(location+5): \ - sprite_fetch(sprite);\ - sprite_fetch(sprite+1); - -#define sprite_y_read(location, sprite) \ - slot(location): \ - posit_sprite(sprite_selection_buffer, sprite, ram_[master_system_.sprite_attribute_table_address & ((sprite) | 0x3f00)], write_pointer_.row); \ - posit_sprite(sprite_selection_buffer, sprite+1, ram_[master_system_.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); \ - line_buffer.names[column].flags = ram_[address+1]; \ - line_buffer.names[column].offset = size_t( \ - (((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) \ - 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) \ - external_slot(location+1); \ - slot(location+2): \ - slot(location+3): \ - slot(location+4): \ - fetch_tile(column) \ - fetch_tile_name(column+1, row_info) \ - sprite_y_read(location+5, sprite); \ - slot(location+6): \ - slot(location+7): \ - slot(location+8): \ - fetch_tile(column+1) \ - fetch_tile_name(column+2, row_info) \ - sprite_y_read(location+9, sprite+2); \ - slot(location+10): \ - slot(location+11): \ - slot(location+12): \ - fetch_tile(column+2) \ - fetch_tile_name(column+3, row_info) \ - sprite_y_read(location+13, sprite+4); \ - slot(location+14): \ - 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. - LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; - LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines]; - const int horizontal_offset = (write_pointer_.row >= 16 || !master_system_.horizontal_scroll_lock) ? (line_buffer.latched_horizontal_scroll >> 3) : 0; - - // Limit address bits in use if this is a SMS2 mode. - const bool is_tall_mode = mode_timing_.pixel_lines != 192; - const size_t pattern_name_address = master_system_.pattern_name_address | (is_tall_mode ? 0x800 : 0); - const size_t pattern_name_offset = is_tall_mode ? 0x100 : 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 = (write_pointer_.row + master_system_.latched_vertical_scroll) % (is_tall_mode ? 256 : 224); - struct RowInfo { - size_t pattern_address_base; - size_t sub_row[2]; - }; - const RowInfo scrolled_row_info = { - (pattern_name_address & size_t(((scrolled_row & ~7) << 3) | 0x3800)) - pattern_name_offset, - {size_t((scrolled_row & 7) << 2), 28 ^ size_t((scrolled_row & 7) << 2)} - }; - RowInfo row_info; - if(master_system_.vertical_scroll_lock) { - row_info.pattern_address_base = (pattern_name_address & size_t(((write_pointer_.row & ~7) << 3) | 0x3800)) - pattern_name_offset; - 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); - - sprite_fetch_block(0, 0); - sprite_fetch_block(6, 2); - - external_slots_4(12); - external_slot(16); - - sprite_fetch_block(17, 4); - sprite_fetch_block(23, 6); - - slot(29): - sprite_selection_buffer.reset_sprite_collection(); - do_external_slot(29*2); - external_slot(30); - - sprite_y_read(31, 0); - sprite_y_read(32, 2); - sprite_y_read(33, 4); - sprite_y_read(34, 6); - sprite_y_read(35, 8); - sprite_y_read(36, 10); - sprite_y_read(37, 12); - sprite_y_read(38, 14); - - background_fetch_block(39, 0, 16, scrolled_row_info); - background_fetch_block(55, 4, 22, scrolled_row_info); - background_fetch_block(71, 8, 28, scrolled_row_info); - background_fetch_block(87, 12, 34, scrolled_row_info); - background_fetch_block(103, 16, 40, scrolled_row_info); - background_fetch_block(119, 20, 46, scrolled_row_info); - background_fetch_block(135, 24, 52, row_info); - background_fetch_block(151, 28, 58, row_info); - - external_slots_4(167); - - return; - } - -#undef background_fetch_block -#undef fetch_tile -#undef fetch_tile_name -#undef sprite_y_read -#undef sprite_fetch_block -#undef sprite_fetch - } - -#undef external_slot -#undef slot - - uint32_t *pixel_target_ = nullptr, *pixel_origin_ = nullptr; - bool asked_for_write_area_ = false; - void draw_tms_character(int start, int end); - void draw_tms_text(int start, int end); - void draw_sms(int start, int end, uint32_t cram_dot); + // Output serialisers. + void draw_tms_character(int start, int end); + void draw_tms_text(int start, int end); + void draw_sms(int start, int end, uint32_t cram_dot); }; +#include "Fetch.hpp" +#include "Draw.hpp" + } } diff --git a/Components/9918/Implementation/ClockConverter.hpp b/Components/9918/Implementation/ClockConverter.hpp new file mode 100644 index 000000000..641f4db8d --- /dev/null +++ b/Components/9918/Implementation/ClockConverter.hpp @@ -0,0 +1,180 @@ +// +// ClockConverter.hpp +// Clock Signal +// +// Created by Thomas Harte on 01/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef ClockConverter_hpp +#define ClockConverter_hpp + +#include "../9918.hpp" +#include "PersonalityTraits.hpp" + +namespace TI { +namespace TMS { + +enum class Clock { + Internal, + TMSPixel, + TMSMemoryWindow, + CRT +}; + +template constexpr int clock_rate() { + static_assert( + is_classic_vdp(personality) || + is_yamaha_vdp(personality) || + (personality == Personality::MDVDP) + ); + + switch(clk) { + case Clock::TMSPixel: return 342; + case Clock::TMSMemoryWindow: return 171; + case Clock::CRT: return 1368; + case Clock::Internal: + if constexpr (is_classic_vdp(personality)) { + return 342; + } else if constexpr (is_yamaha_vdp(personality)) { + return 1368; + } else if constexpr (personality == Personality::MDVDP) { + return 3420; + } + } +} + +template constexpr int to_internal(int length) { + return length * clock_rate() / clock_rate(); +} + +template constexpr int from_internal(int length) { + return length * clock_rate() / clock_rate(); +} + +/// Provides default timing measurements that duplicate the layout of a TMS9928's line, +/// scaled to the clock rate specified. +template struct StandardTiming { + /// The total number of internal cycles per line of output. + constexpr static int CyclesPerLine = clock_rate(); + + /// The number of internal cycles that must elapse between a request to read or write and + /// it becoming a candidate for action. + constexpr static int VRAMAccessDelay = 6; + + /// The first internal cycle at which pixels will be output in any mode other than text. + /// Pixels implicitly run from here to the end of the line. + constexpr static int FirstPixelCycle = 86 * CyclesPerLine / 342; + + /// The first internal cycle at which pixels will be output text mode. + constexpr static int FirstTextCycle = 94 * CyclesPerLine / 342; + + /// The final internal cycle at which pixels will be output text mode. + constexpr static int LastTextCycle = 334 * CyclesPerLine / 342; + + // For the below, the fixed portion of line layout is: + // + // [0, EndOfRightBorder): right border colour + // [EndOfRightBorder, StartOfSync): blank + // [StartOfSync, EndOfSync): sync + // [EndOfSync, StartOfColourBurst): blank + // [StartOfColourBurst, EndOfColourBurst): the colour burst + // [EndOfColourBurst, StartOfLeftBorder): blank + // + // The region from StartOfLeftBorder until the end is then filled with + // some combination of pixels and more border, depending on the vertical + // position of this line and the current screen mode. + constexpr static int EndOfRightBorder = 15 * CyclesPerLine / 342; + constexpr static int StartOfSync = 23 * CyclesPerLine / 342; + constexpr static int EndOfSync = 49 * CyclesPerLine / 342; + constexpr static int StartOfColourBurst = 51 * CyclesPerLine / 342; + constexpr static int EndOfColourBurst = 65 * CyclesPerLine / 342; + constexpr static int StartOfLeftBorder = 73 * CyclesPerLine / 342; +}; + +/// Provides concrete, specific timing for the nominated personality. +template struct Timing: public StandardTiming {}; + +/*! + Provides a [potentially-]stateful conversion between the external and internal clocks. + Unlike the other clock conversions, this one may be non-integral, requiring that + an error term be tracked. +*/ +template class ClockConverter { + public: + /*! + Given that another @c source external **half-cycles** has occurred, + indicates how many complete internal **cycles** have additionally elapsed + since the last call to @c to_internal. + + E.g. for the TMS, @c source will count 456 ticks per line, and the internal clock + runs at 342 ticks per line, so the proper conversion is to multiply by 3/4. + */ + int to_internal(int source) { + switch(personality) { + // Default behaviour is to apply a multiplication by 3/4; + // this is correct for the TMS and Sega VDPs other than the Mega Drive. + default: { + const int result = source * 3 + cycles_error_; + cycles_error_ = result & 3; + return result >> 2; + } + + // The two Yamaha chips have an internal clock that is four times + // as fast as the TMS, therefore a stateless translation is possible. + case Personality::V9938: + case Personality::V9958: + return source * 3; + + // The Mega Drive runs at 3420 master clocks per line, which is then + // divided by 4 or 5 depending on other state. That's 7 times the + // rate provided to the CPU; given that the input is in half-cycles + // the proper multiplier is therefore 3.5. + case Personality::MDVDP: { + const int result = source * 7 + cycles_error_; + cycles_error_ = result & 1; + return result >> 1; + } + } + } + + /*! + Provides the number of external cycles that need to begin from now in order to + get at least @c internal_cycles into the future. + */ + HalfCycles half_cycles_before_internal_cycles(int internal_cycles) const { + // Logic here correlates with multipliers as per @c to_internal. + switch(personality) { + default: + // Relative to the external clock multiplied by 3, it will definitely take this + // many cycles to complete a further (internal_cycles - 1) after the current one. + internal_cycles = (internal_cycles - 1) << 2; + + // It will also be necessary to complete the current one. + internal_cycles += 4 - cycles_error_; + + // Round up to get the first external cycle after + // the number of internal_cycles has elapsed. + return HalfCycles((internal_cycles + 2) / 3); + + case Personality::V9938: + case Personality::V9958: + return HalfCycles((internal_cycles + 2) / 3); + + case Personality::MDVDP: + internal_cycles = (internal_cycles - 1) << 1; + internal_cycles += 2 - cycles_error_; + return HalfCycles((internal_cycles + 6) / 7); + } + } + + private: + // Holds current residue in conversion from the external to + // internal clock. + int cycles_error_ = 0; +}; + +} +} + +#endif /* ClockConverter_hpp */ diff --git a/Components/9918/Implementation/Draw.hpp b/Components/9918/Implementation/Draw.hpp new file mode 100644 index 000000000..4fe6da102 --- /dev/null +++ b/Components/9918/Implementation/Draw.hpp @@ -0,0 +1,293 @@ +// +// Draw.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef Draw_hpp +#define Draw_hpp + +// MARK: - TMS9918 + +template +void Base::draw_tms_character(int start, int end) { + LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; + + // Paint the background tiles. + const int pixels_left = end - start; + if(this->screen_mode_ == ScreenMode::MultiColour) { + for(int c = start; c < end; ++c) { + pixel_target_[c] = palette[ + (line_buffer.patterns[c >> 3][0] >> (((c & 4)^4))) & 15 + ]; + } + } else { + const int shift = start & 7; + int byte_column = start >> 3; + + int length = std::min(pixels_left, 8 - shift); + + int pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]) >> shift; + uint8_t colour = line_buffer.patterns[byte_column][1]; + uint32_t colours[2] = { + palette[(colour & 15) ? (colour & 15) : background_colour_], + palette[(colour >> 4) ? (colour >> 4) : background_colour_] + }; + + int background_pixels_left = pixels_left; + while(true) { + background_pixels_left -= length; + for(int c = 0; c < length; ++c) { + pixel_target_[c] = colours[pattern&0x01]; + pattern >>= 1; + } + pixel_target_ += length; + + if(!background_pixels_left) break; + length = std::min(8, background_pixels_left); + byte_column++; + + pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]); + colour = line_buffer.patterns[byte_column][1]; + colours[0] = palette[(colour & 15) ? (colour & 15) : background_colour_]; + colours[1] = palette[(colour >> 4) ? (colour >> 4) : background_colour_]; + } + } + + // Paint sprites and check for collisions, but only if at least one sprite is active + // on this line. + if(line_buffer.active_sprite_slot) { + const int shift_advance = sprites_magnified_ ? 1 : 2; + // If this is the start of the line clip any part of any sprites that is off to the left. + if(!start) { + for(int index = 0; index < line_buffer.active_sprite_slot; ++index) { + LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; + if(sprite.x < 0) sprite.shift_position -= shift_advance * sprite.x; + } + } + + int sprite_buffer[256]; + int sprite_collision = 0; + memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); + + constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; + constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + + // Draw all sprites into the sprite buffer. + const int shifter_target = sprites_16x16_ ? 32 : 16; + for(int index = line_buffer.active_sprite_slot - 1; index >= 0; --index) { + LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; + if(sprite.shift_position < shifter_target) { + const int pixel_start = std::max(start, sprite.x); + for(int c = pixel_start; c < end && sprite.shift_position < shifter_target; ++c) { + const int shift = (sprite.shift_position >> 1) ^ 7; + int sprite_colour = (sprite.image[shift >> 3] >> (shift & 7)) & 1; + + // A colision is detected regardless of sprite colour ... + sprite_collision |= sprite_buffer[c] & sprite_colour; + sprite_buffer[c] |= sprite_colour; + + // ... but a sprite with the transparent colour won't actually be visible. + sprite_colour &= colour_masks[sprite.image[2]&15]; + pixel_origin_[c] = + (pixel_origin_[c] & sprite_colour_selection_masks[sprite_colour^1]) | + (palette[sprite.image[2]&15] & sprite_colour_selection_masks[sprite_colour]); + + sprite.shift_position += shift_advance; + } + } + } + + status_ |= sprite_collision << StatusSpriteCollisionShift; + } +} + +template +void Base::draw_tms_text(int start, int end) { + LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; + const uint32_t colours[2] = { palette[background_colour_], palette[text_colour_] }; + + const int shift = start % 6; + int byte_column = start / 6; + int pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]) >> shift; + int pixels_left = end - start; + int length = std::min(pixels_left, 6 - shift); + while(true) { + pixels_left -= length; + for(int c = 0; c < length; ++c) { + pixel_target_[c] = colours[pattern&0x01]; + pattern >>= 1; + } + pixel_target_ += length; + + if(!pixels_left) break; + length = std::min(6, pixels_left); + byte_column++; + pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]); + } +} + +// MARK: - Master System + +template +void Base::draw_sms(int start, int end, uint32_t cram_dot) { + LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; + int colour_buffer[256]; + + /* + Add extra border for any pixels that fall before the fine scroll. + */ + int tile_start = start, tile_end = end; + int tile_offset = start; + 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 - (line_buffer.latched_horizontal_scroll & 7), 0); + tile_end = std::max(end - (line_buffer.latched_horizontal_scroll & 7), 0); + } + + + uint32_t pattern; + uint8_t *const pattern_index = reinterpret_cast(&pattern); + + /* + Add background tiles; these will fill the colour_buffer with values in which + the low five bits are a palette index, and bit six is set if this tile has + priority over sprites. + */ + if(tile_start < end) { + const int shift = tile_start & 7; + int byte_column = tile_start >> 3; + int pixels_left = tile_end - tile_start; + int length = std::min(pixels_left, 8 - shift); + + 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 = (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) | + ((pattern_index[2] & 0x01) << 2) | + ((pattern_index[1] & 0x01) << 1) | + ((pattern_index[0] & 0x01) << 0) | + palette_offset; + ++tile_offset; + pattern >>= 1; + } + } else { + for(int c = 0; c < length; ++c) { + colour_buffer[tile_offset] = + ((pattern_index[3] & 0x80) >> 4) | + ((pattern_index[2] & 0x80) >> 5) | + ((pattern_index[1] & 0x80) >> 6) | + ((pattern_index[0] & 0x80) >> 7) | + palette_offset; + ++tile_offset; + pattern <<= 1; + } + } + + pixels_left -= length; + if(!pixels_left) break; + + length = std::min(8, pixels_left); + byte_column++; + pattern = *reinterpret_cast(line_buffer.patterns[byte_column]); + } + } + + /* + Apply sprites (if any). + */ + if(line_buffer.active_sprite_slot) { + const int shift_advance = sprites_magnified_ ? 1 : 2; + + // If this is the start of the line clip any part of any sprites that is off to the left. + if(!start) { + for(int index = 0; index < line_buffer.active_sprite_slot; ++index) { + LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; + if(sprite.x < 0) sprite.shift_position -= shift_advance * sprite.x; + } + } + + int sprite_buffer[256]; + int sprite_collision = 0; + memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); + + // Draw all sprites into the sprite buffer. + 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); + + // TODO: it feels like the work below should be simplifiable; + // the double shift in particular, and hopefully the variable shift. + for(int c = pixel_start; c < end && sprite.shift_position < 16; ++c) { + const int shift = (sprite.shift_position >> 1); + const int sprite_colour = + (((sprite.image[3] << shift) & 0x80) >> 4) | + (((sprite.image[2] << shift) & 0x80) >> 5) | + (((sprite.image[1] << shift) & 0x80) >> 6) | + (((sprite.image[0] << shift) & 0x80) >> 7); + + if(sprite_colour) { + sprite_collision |= sprite_buffer[c]; + sprite_buffer[c] = sprite_colour | 0x10; + } + + sprite.shift_position += shift_advance; + } + } + } + + // Draw the sprite buffer onto the colour buffer, wherever the tile map doesn't have + // priority (or is transparent). + for(int c = start; c < end; ++c) { + if( + sprite_buffer[c] && + (!(colour_buffer[c]&0x20) || !(colour_buffer[c]&0xf)) + ) colour_buffer[c] = sprite_buffer[c]; + } + + if(sprite_collision) + status_ |= StatusSpriteCollision; + } + + // Map from the 32-colour buffer to real output pixels, applying the specific CRAM dot if any. + pixel_target_[start] = master_system_.colour_ram[colour_buffer[start] & 0x1f] | cram_dot; + for(int c = start+1; c < end; ++c) { + pixel_target_[c] = master_system_.colour_ram[colour_buffer[c] & 0x1f]; + } + + // If the VDP is set to hide the left column and this is the final call that'll come + // this line, hide it. + if(end == 256) { + if(master_system_.hide_left_column) { + pixel_origin_[0] = pixel_origin_[1] = pixel_origin_[2] = pixel_origin_[3] = + pixel_origin_[4] = pixel_origin_[5] = pixel_origin_[6] = pixel_origin_[7] = + master_system_.colour_ram[16 + background_colour_]; + } + } +} + +// MARK: - Yamaha + +// TODO. + +// MARK: - Mega Drive + +// TODO. + +#endif /* Draw_hpp */ diff --git a/Components/9918/Implementation/Fetch.hpp b/Components/9918/Implementation/Fetch.hpp new file mode 100644 index 000000000..a2367de93 --- /dev/null +++ b/Components/9918/Implementation/Fetch.hpp @@ -0,0 +1,497 @@ +// +// Fetch.hpp +// Clock Signal +// +// Created by Thomas Harte on 01/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef Fetch_hpp +#define Fetch_hpp + +/* + Fetching routines follow below; they obey the following rules: + + 1) input is a start position and an end position; they should perform the proper + operations for the period: start <= time < end. + 2) times are measured relative to a 172-cycles-per-line clock (so: they directly + count access windows on the TMS and Master System). + 3) time 0 is the beginning of the access window immediately after the last pattern/data + block fetch that would contribute to this line, in a normal 32-column mode. So: + + * it's cycle 309 on Mattias' TMS diagram; + * it's cycle 1238 on his V9938 diagram; + * it's after the last background render block in Mask of Destiny's Master System timing diagram. + + That division point was selected, albeit arbitrarily, because it puts all the tile + fetches for a single line into the same [0, 171] period. + + 4) all of these functions are templated with a `use_end` parameter. That will be true if + end is < 172, false otherwise. So functions can use it to eliminate should-exit-not checks, + for the more usual path of execution. + + Provided for the benefit of the methods below: + + * the function external_slot(), which will perform any pending VRAM read/write. + * the macros slot(n) and external_slot(n) which can be used to schedule those things inside a + switch(start)-based implementation. + + All functions should just spool data to intermediary storage. This is because for most VDPs there is + a decoupling between fetch pattern and output pattern, and it's neater to keep the same division + for the exceptions. +*/ + +#define slot(n) \ + if(use_end && end == n) return; \ + [[fallthrough]]; \ + case n + +#define external_slot(n) \ + slot(n): do_external_slot(to_internal(n)); + +#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); + + +// MARK: - TMS9918 + +template +template void Base::fetch_tms_refresh(int start, int end) { +#define refresh(location) \ + slot(location): \ + external_slot(location+1); + +#define refreshes_2(location) \ + refresh(location); \ + refresh(location+2); + +#define refreshes_4(location) \ + refreshes_2(location); \ + refreshes_2(location+4); + +#define refreshes_8(location) \ + refreshes_4(location); \ + refreshes_4(location+8); + + switch(start) { + default: assert(false); + + /* 44 external slots */ + external_slots_32(0) + external_slots_8(32) + external_slots_4(40) + + /* 64 refresh/external slot pairs (= 128 windows) */ + refreshes_8(44); + refreshes_8(60); + refreshes_8(76); + refreshes_8(92); + refreshes_8(108); + refreshes_8(124); + refreshes_8(140); + refreshes_8(156); + + return; + } + +#undef refreshes_8 +#undef refreshes_4 +#undef refreshes_2 +#undef refresh +} + +template +template void Base::fetch_tms_text(int start, int end) { +#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); \ + external_slot(location+1); \ + fetch_tile_pattern(location+2, column); + +#define fetch_columns_2(location, column) \ + fetch_column(location, column); \ + fetch_column(location+3, column+1); + +#define fetch_columns_4(location, column) \ + fetch_columns_2(location, column); \ + fetch_columns_2(location+6, column+2); + +#define fetch_columns_8(location, column) \ + fetch_columns_4(location, column); \ + fetch_columns_4(location+12, column+4); + + LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; + const size_t row_base = pattern_name_address_ & (0x3c00 | size_t(write_pointer_.row >> 3) * 40); + const size_t row_offset = pattern_generator_table_address_ & (0x3800 | (write_pointer_.row & 7)); + + switch(start) { + default: assert(false); + + /* 47 external slots (= 47 windows) */ + external_slots_32(0) + external_slots_8(32) + external_slots_4(40) + external_slots_2(44) + external_slot(46) + + /* 40 column fetches (= 120 windows) */ + fetch_columns_8(47, 0); + fetch_columns_8(71, 8); + fetch_columns_8(95, 16); + fetch_columns_8(119, 24); + fetch_columns_8(143, 32); + + /* 5 more external slots */ + external_slots_4(167); + external_slot(171); + + return; + } + +#undef fetch_columns_8 +#undef fetch_columns_4 +#undef fetch_columns_2 +#undef fetch_column +#undef fetch_tile_pattern +#undef fetch_tile_name +} + +template +template void Base::fetch_tms_character(int start, int end) { +#define sprite_fetch_coordinates(location, sprite) \ + slot(location): \ + slot(location+1): \ + line_buffer.active_sprites[sprite].x = \ + ram_[\ + sprite_attribute_table_address_ & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 2))\ + ]; + + // This implementation doesn't refetch Y; it's unclear to me + // whether it's refetched. + +#define sprite_fetch_graphics(location, sprite) \ + slot(location): \ + slot(location+1): \ + slot(location+2): \ + slot(location+3): {\ + const uint8_t name = ram_[\ + sprite_attribute_table_address_ & size_t(0x3f82 | (line_buffer.active_sprites[sprite].index << 2))\ + ] & (sprites_16x16_ ? ~3 : ~0);\ + line_buffer.active_sprites[sprite].image[2] = ram_[\ + sprite_attribute_table_address_ & size_t(0x3f83 | (line_buffer.active_sprites[sprite].index << 2))\ + ];\ + line_buffer.active_sprites[sprite].x -= (line_buffer.active_sprites[sprite].image[2] & 0x80) >> 2;\ + const size_t graphic_location = sprite_generator_table_address_ & size_t(0x3800 | (name << 3) | line_buffer.active_sprites[sprite].row); \ + line_buffer.active_sprites[sprite].image[0] = ram_[graphic_location];\ + line_buffer.active_sprites[sprite].image[1] = ram_[graphic_location+16];\ + } + +#define sprite_fetch_block(location, sprite) \ + sprite_fetch_coordinates(location, sprite) \ + sprite_fetch_graphics(location+2, sprite) + +#define sprite_y_read(location, sprite) \ + slot(location): posit_sprite(sprite_selection_buffer, sprite, ram_[sprite_attribute_table_address_ & (((sprite) << 2) | 0x3f80)], write_pointer_.row); + +#define fetch_tile_name(column) line_buffer.names[column].offset = ram_[(row_base + column) & 0x3fff]; + +#define fetch_tile(column) {\ + line_buffer.patterns[column][1] = ram_[(colour_base + size_t((line_buffer.names[column].offset << 3) >> colour_name_shift)) & 0x3fff]; \ + line_buffer.patterns[column][0] = ram_[(pattern_base + size_t(line_buffer.names[column].offset << 3)) & 0x3fff]; \ + } + +#define background_fetch_block(location, column, sprite) \ + slot(location): fetch_tile_name(column) \ + external_slot(location+1); \ + slot(location+2): \ + slot(location+3): fetch_tile(column) \ + slot(location+4): fetch_tile_name(column+1) \ + sprite_y_read(location+5, sprite); \ + slot(location+6): \ + slot(location+7): fetch_tile(column+1) \ + slot(location+8): fetch_tile_name(column+2) \ + sprite_y_read(location+9, sprite+1); \ + slot(location+10): \ + slot(location+11): fetch_tile(column+2) \ + slot(location+12): fetch_tile_name(column+3) \ + sprite_y_read(location+13, sprite+2); \ + slot(location+14): \ + slot(location+15): fetch_tile(column+3) + + LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; + LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines]; + const size_t row_base = pattern_name_address_ & (size_t((write_pointer_.row << 2)&~31) | 0x3c00); + + size_t pattern_base = pattern_generator_table_address_; + size_t colour_base = colour_table_address_; + int colour_name_shift = 6; + + if(screen_mode_ == ScreenMode::Graphics) { + // If this is high resolution mode, allow the row number to affect the pattern and colour addresses. + pattern_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5)); + colour_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5)); + + colour_base += size_t(write_pointer_.row & 7); + colour_name_shift = 0; + } else { + colour_base &= size_t(0xffc0); + pattern_base &= size_t(0x3800); + } + + if(screen_mode_ == ScreenMode::MultiColour) { + pattern_base += size_t((write_pointer_.row >> 2) & 7); + } else { + pattern_base += size_t(write_pointer_.row & 7); + } + + switch(start) { + default: assert(false); + + external_slots_2(0); + + sprite_fetch_block(2, 0); + sprite_fetch_block(8, 1); + sprite_fetch_coordinates(14, 2); + + external_slots_4(16); + external_slot(20); + + sprite_fetch_graphics(21, 2); + sprite_fetch_block(25, 3); + + slot(31): + sprite_selection_buffer.reset_sprite_collection(); + do_external_slot(to_internal(31)); + external_slots_2(32); + external_slot(34); + + sprite_y_read(35, 0); + sprite_y_read(36, 1); + sprite_y_read(37, 2); + sprite_y_read(38, 3); + sprite_y_read(39, 4); + sprite_y_read(40, 5); + sprite_y_read(41, 6); + sprite_y_read(42, 7); + + background_fetch_block(43, 0, 8); + background_fetch_block(59, 4, 11); + background_fetch_block(75, 8, 14); + background_fetch_block(91, 12, 17); + background_fetch_block(107, 16, 20); + background_fetch_block(123, 20, 23); + background_fetch_block(139, 24, 26); + background_fetch_block(155, 28, 29); + + return; + } + +#undef background_fetch_block +#undef fetch_tile +#undef fetch_tile_name +#undef sprite_y_read +#undef sprite_fetch_block +#undef sprite_fetch_graphics +#undef sprite_fetch_coordinates +} + + +// MARK: - Master System + +template +template void Base::fetch_sms(int start, int end) { +#define sprite_fetch(sprite) {\ + line_buffer.active_sprites[sprite].x = \ + ram_[\ + master_system_.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_[\ + master_system_.sprite_attribute_table_address & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 1))\ + ] & (sprites_16x16_ ? ~1 : ~0);\ + const size_t graphic_location = master_system_.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) \ + slot(location): \ + slot(location+1): \ + slot(location+2): \ + slot(location+3): \ + slot(location+4): \ + slot(location+5): \ + sprite_fetch(sprite);\ + sprite_fetch(sprite+1); + +#define sprite_y_read(location, sprite) \ + slot(location): \ + posit_sprite(sprite_selection_buffer, sprite, ram_[master_system_.sprite_attribute_table_address & ((sprite) | 0x3f00)], write_pointer_.row); \ + posit_sprite(sprite_selection_buffer, sprite+1, ram_[master_system_.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); \ + line_buffer.names[column].flags = ram_[address+1]; \ + line_buffer.names[column].offset = size_t( \ + (((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) \ + 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) \ + external_slot(location+1); \ + slot(location+2): \ + slot(location+3): \ + slot(location+4): \ + fetch_tile(column) \ + fetch_tile_name(column+1, row_info) \ + sprite_y_read(location+5, sprite); \ + slot(location+6): \ + slot(location+7): \ + slot(location+8): \ + fetch_tile(column+1) \ + fetch_tile_name(column+2, row_info) \ + sprite_y_read(location+9, sprite+2); \ + slot(location+10): \ + slot(location+11): \ + slot(location+12): \ + fetch_tile(column+2) \ + fetch_tile_name(column+3, row_info) \ + sprite_y_read(location+13, sprite+4); \ + slot(location+14): \ + 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. + LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; + LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines]; + const int horizontal_offset = (write_pointer_.row >= 16 || !master_system_.horizontal_scroll_lock) ? (line_buffer.latched_horizontal_scroll >> 3) : 0; + + // Limit address bits in use if this is a SMS2 mode. + const bool is_tall_mode = mode_timing_.pixel_lines != 192; + const size_t pattern_name_address = master_system_.pattern_name_address | (is_tall_mode ? 0x800 : 0); + const size_t pattern_name_offset = is_tall_mode ? 0x100 : 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 = (write_pointer_.row + master_system_.latched_vertical_scroll) % (is_tall_mode ? 256 : 224); + struct RowInfo { + size_t pattern_address_base; + size_t sub_row[2]; + }; + const RowInfo scrolled_row_info = { + (pattern_name_address & size_t(((scrolled_row & ~7) << 3) | 0x3800)) - pattern_name_offset, + {size_t((scrolled_row & 7) << 2), 28 ^ size_t((scrolled_row & 7) << 2)} + }; + RowInfo row_info; + if(master_system_.vertical_scroll_lock) { + row_info.pattern_address_base = (pattern_name_address & size_t(((write_pointer_.row & ~7) << 3) | 0x3800)) - pattern_name_offset; + 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); + + sprite_fetch_block(0, 0); + sprite_fetch_block(6, 2); + + external_slots_4(12); + external_slot(16); + + sprite_fetch_block(17, 4); + sprite_fetch_block(23, 6); + + slot(29): + sprite_selection_buffer.reset_sprite_collection(); + do_external_slot(to_internal(29)); + external_slot(30); + + sprite_y_read(31, 0); + sprite_y_read(32, 2); + sprite_y_read(33, 4); + sprite_y_read(34, 6); + sprite_y_read(35, 8); + sprite_y_read(36, 10); + sprite_y_read(37, 12); + sprite_y_read(38, 14); + + background_fetch_block(39, 0, 16, scrolled_row_info); + background_fetch_block(55, 4, 22, scrolled_row_info); + background_fetch_block(71, 8, 28, scrolled_row_info); + background_fetch_block(87, 12, 34, scrolled_row_info); + background_fetch_block(103, 16, 40, scrolled_row_info); + background_fetch_block(119, 20, 46, scrolled_row_info); + background_fetch_block(135, 24, 52, row_info); + background_fetch_block(151, 28, 58, row_info); + + external_slots_4(167); + + return; + } + +#undef background_fetch_block +#undef fetch_tile +#undef fetch_tile_name +#undef sprite_y_read +#undef sprite_fetch_block +#undef sprite_fetch +} + +// MARK: - Yamaha + +// TODO. + +template +template void Base::fetch_yamaha_refresh(int start, int end) { + (void)start; + (void)end; +} + +template +template void Base::fetch_yamaha_no_sprites(int start, int end) { + (void)start; + (void)end; +} + +template +template void Base::fetch_yamaha_sprites(int start, int end) { + (void)start; + (void)end; +} + +// MARK: - Mega Drive + +// TODO. + +#undef external_slot +#undef slot + +#endif /* Fetch_hpp */ diff --git a/Components/9918/Implementation/PersonalityTraits.hpp b/Components/9918/Implementation/PersonalityTraits.hpp new file mode 100644 index 000000000..6a6afdac0 --- /dev/null +++ b/Components/9918/Implementation/PersonalityTraits.hpp @@ -0,0 +1,49 @@ +// +// PersonalityTraits.hpp +// Clock Signal +// +// Created by Thomas Harte on 06/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef PersonalityTraits_hpp +#define PersonalityTraits_hpp + +namespace TI { +namespace TMS { + +// Genus determinants for the various personalityes. +constexpr bool is_sega_vdp(Personality p) { + return p >= Personality::SMSVDP; +} + +constexpr bool is_yamaha_vdp(Personality p) { + return p == Personality::V9938 || p == Personality::V9958; +} + +// i.e. one with the original internal timings. +constexpr bool is_classic_vdp(Personality p) { + return + p == Personality::TMS9918A || + p == Personality::SMSVDP || + p == Personality::SMS2VDP || + p == Personality::GGVDP; +} + +constexpr size_t memory_size(Personality p) { + switch(p) { + case TI::TMS::TMS9918A: + case TI::TMS::SMSVDP: + case TI::TMS::SMS2VDP: + case TI::TMS::GGVDP: return 16 * 1024; + case TI::TMS::MDVDP: return 64 * 1024; + case TI::TMS::V9938: return 128 * 1024; + case TI::TMS::V9958: return 192 * 1024; + } +} + +} +} + + +#endif /* PersonalityTraits_hpp */ diff --git a/Machines/ColecoVision/ColecoVision.cpp b/Machines/ColecoVision/ColecoVision.cpp index 2138528a9..8349bbef6 100644 --- a/Machines/ColecoVision/ColecoVision.cpp +++ b/Machines/ColecoVision/ColecoVision.cpp @@ -117,7 +117,6 @@ class ConcreteMachine: public: ConcreteMachine(const Analyser::Static::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : z80_(*this), - vdp_(TI::TMS::TMS9918A), sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, sn76489_divider), ay_(GI::AY38910::Personality::AY38910, audio_queue_), mixer_(sn76489_, ay_), @@ -379,7 +378,7 @@ class ConcreteMachine: } CPU::Z80::Processor z80_; - JustInTimeActor vdp_; + JustInTimeActor> vdp_; Concurrency::AsyncTaskQueue audio_queue_; TI::SN76489 sn76489_; diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp index 88e879601..6bc06bda0 100644 --- a/Machines/MSX/MSX.cpp +++ b/Machines/MSX/MSX.cpp @@ -146,7 +146,6 @@ class ConcreteMachine: ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher): z80_(*this), - vdp_(TI::TMS::TMS9918A), i8255_(i8255_port_handler_), ay_(GI::AY38910::Personality::AY38910, audio_queue_), audio_toggle_(audio_queue_), @@ -744,7 +743,7 @@ class ConcreteMachine: }; CPU::Z80::Processor z80_; - JustInTimeActor vdp_; + JustInTimeActor> vdp_; Intel::i8255::i8255 i8255_; Concurrency::AsyncTaskQueue audio_queue_; diff --git a/Machines/MasterSystem/MasterSystem.cpp b/Machines/MasterSystem/MasterSystem.cpp index da26f9775..1a4c38be0 100644 --- a/Machines/MasterSystem/MasterSystem.cpp +++ b/Machines/MasterSystem/MasterSystem.cpp @@ -29,6 +29,7 @@ #include "../../Analyser/Static/Sega/Target.hpp" #include +#include #include namespace { @@ -77,7 +78,7 @@ class Joystick: public Inputs::ConcreteJoystick { uint8_t state_ = 0xff; }; -class ConcreteMachine: +template class ConcreteMachine: public Machine, public CPU::Z80::BusHandler, public MachineTypes::TimedMachine, @@ -90,11 +91,9 @@ class ConcreteMachine: public: ConcreteMachine(const Analyser::Static::Sega::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : - model_(target.model), region_(target.region), paging_scheme_(target.paging_scheme), z80_(*this), - vdp_(tms_personality_for_model(target.model)), sn76489_( (target.model == Target::Model::SG1000) ? TI::SN76489::Personality::SN76489 : TI::SN76489::Personality::SMS, audio_queue_, @@ -159,7 +158,7 @@ class ConcreteMachine: page_cartridge(); // Map RAM. - if(is_master_system(model_)) { + if constexpr (is_master_system(model)) { map(read_pointers_, ram_, 8*1024, 0xc000, 0x10000); map(write_pointers_, ram_, 8*1024, 0xc000, 0x10000); } else { @@ -311,7 +310,7 @@ class ConcreteMachine: case CPU::Z80::PartialMachineCycle::Output: switch(address & 0xc1) { case 0x00: // i.e. even ports less than 0x40. - if(is_master_system(model_)) { + if constexpr (is_master_system(model)) { // TODO: Obey the RAM enable. LOG("Memory control: " << PADHEX(2) << memory_control_); memory_control_ = *cycle.value; @@ -431,7 +430,7 @@ class ConcreteMachine: } private: - static TI::TMS::Personality tms_personality_for_model(Analyser::Static::Sega::Target::Model model) { + static constexpr TI::TMS::Personality tms_personality() { switch(model) { default: case Target::Model::SG1000: return TI::TMS::TMS9918A; @@ -481,11 +480,10 @@ class ConcreteMachine: } using Target = Analyser::Static::Sega::Target; - const Target::Model model_; const Target::Region region_; const Target::PagingScheme paging_scheme_; CPU::Z80::Processor z80_; - JustInTimeActor vdp_; + JustInTimeActor> vdp_; Concurrency::AsyncTaskQueue audio_queue_; TI::SN76489 sn76489_; @@ -559,7 +557,14 @@ using namespace Sega::MasterSystem; Machine *Machine::MasterSystem(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { using Target = Analyser::Static::Sega::Target; const Target *const sega_target = dynamic_cast(target); - return new ConcreteMachine(*sega_target, rom_fetcher); + + switch(sega_target->model) { + case Target::Model::SG1000: return new ConcreteMachine(*sega_target, rom_fetcher); + case Target::Model::MasterSystem: return new ConcreteMachine(*sega_target, rom_fetcher); + case Target::Model::MasterSystem2: return new ConcreteMachine(*sega_target, rom_fetcher); + default: + assert(false); + } } Machine::~Machine() {} diff --git a/Numeric/BitReverse.hpp b/Numeric/BitReverse.hpp new file mode 100644 index 000000000..440501f1b --- /dev/null +++ b/Numeric/BitReverse.hpp @@ -0,0 +1,62 @@ +// +// BitReverse.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef BitReverse_hpp +#define BitReverse_hpp + +#include + +namespace Numeric { + +/// @returns @c source with the order of its bits reversed. E.g. if @c IntT is @c uint8_t then +/// the reverse of bit pattern abcd efgh is hgfd dcba. +template constexpr IntT bit_reverse(IntT source); + +// The single-byte specialisation uses a lookup table. +template<> constexpr uint8_t bit_reverse(uint8_t source) { + struct ReverseTable { + static constexpr std::array reverse_table() { + std::array map{}; + for(std::size_t c = 0; c < 256; ++c) { + map[c] = uint8_t( + ((c & 0x80) >> 7) | + ((c & 0x40) >> 5) | + ((c & 0x20) >> 3) | + ((c & 0x10) >> 1) | + ((c & 0x08) << 1) | + ((c & 0x04) << 3) | + ((c & 0x02) << 5) | + ((c & 0x01) << 7) + ); + } + return map; + } + }; + + const std::array map = ReverseTable::reverse_table(); + return map[source]; +} + +// All other versions just call the byte-level reverse the appropriate number of times. +template constexpr IntT bit_reverse(IntT source) { + IntT result; + + uint8_t *src = reinterpret_cast(&source); + uint8_t *dest = reinterpret_cast(&result) + sizeof(result) - 1; + for(size_t c = 0; c < sizeof(source); c++) { + *dest = bit_reverse(*src); + ++src; + --dest; + } + + return result; +} + +} + +#endif /* BitReverse_hpp */ diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 8040034f9..dcf95902c 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -137,8 +137,6 @@ 4B0E04EA1FC9E5DA00F43484 /* CAS.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E04E81FC9E5DA00F43484 /* CAS.cpp */; }; 4B0E04EB1FC9E78800F43484 /* CAS.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E04E81FC9E5DA00F43484 /* CAS.cpp */; }; 4B0E04F11FC9EA9500F43484 /* MSX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B79A4FF1FC913C900EEDAD5 /* MSX.cpp */; }; - 4B0E04FA1FC9FA3100F43484 /* 9918.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E04F91FC9FA3100F43484 /* 9918.cpp */; }; - 4B0E04FB1FC9FA3100F43484 /* 9918.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E04F91FC9FA3100F43484 /* 9918.cpp */; }; 4B0E61071FF34737002A9DBD /* MSX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E61051FF34737002A9DBD /* MSX.cpp */; }; 4B0F1BB22602645900B85C66 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1BB02602645900B85C66 /* StaticAnalyser.cpp */; }; 4B0F1BB32602645900B85C66 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0F1BB02602645900B85C66 /* StaticAnalyser.cpp */; }; @@ -229,6 +227,9 @@ 4B3F76B925A1635300178AEC /* PowerPCDecoderTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B3F76B825A1635300178AEC /* PowerPCDecoderTests.mm */; }; 4B3FCC40201EC24200960631 /* MultiMachine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3FCC3F201EC24200960631 /* MultiMachine.cpp */; }; 4B3FE75E1F3CF68B00448EE4 /* CPM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3FE75C1F3CF68B00448EE4 /* CPM.cpp */; }; + 4B43983929620FC7006B0BFC /* 9918.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B43983829620FB1006B0BFC /* 9918.cpp */; }; + 4B43983A29620FC8006B0BFC /* 9918.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B43983829620FB1006B0BFC /* 9918.cpp */; }; + 4B43983B29620FC9006B0BFC /* 9918.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B43983829620FB1006B0BFC /* 9918.cpp */; }; 4B448E811F1C45A00009ABD6 /* TZX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B448E7F1F1C45A00009ABD6 /* TZX.cpp */; }; 4B448E841F1C4C480009ABD6 /* PulseQueuedTape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */; }; 4B44EBF51DC987AF00A7820C /* AllSuiteA.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4B44EBF41DC987AE00A7820C /* AllSuiteA.bin */; }; @@ -352,7 +353,6 @@ 4B7752C228217F5C0073E2C5 /* Spectrum.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5D5C9525F56FC7001B4623 /* Spectrum.cpp */; }; 4B7752C328217F720073E2C5 /* Z80.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DD39526360DDF00B3C866 /* Z80.cpp */; }; 4B778EF023A5D68C0000D260 /* 68000Storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BFF1D3822337B0300838EA1 /* 68000Storage.cpp */; }; - 4B778EF123A5D6B50000D260 /* 9918.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E04F91FC9FA3100F43484 /* 9918.cpp */; }; 4B778EF323A5DB230000D260 /* PCMSegment.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4518731F75E91800926311 /* PCMSegment.cpp */; }; 4B778EF423A5DB3A0000D260 /* C1540.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8334941F5E25B60097E338 /* C1540.cpp */; }; 4B778EF523A5DB440000D260 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B894517201967B4007DE474 /* StaticAnalyser.cpp */; }; @@ -1179,7 +1179,6 @@ 4B0E04E81FC9E5DA00F43484 /* CAS.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CAS.cpp; sourceTree = ""; }; 4B0E04E91FC9E5DA00F43484 /* CAS.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CAS.hpp; sourceTree = ""; }; 4B0E04F81FC9FA3000F43484 /* 9918.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = 9918.hpp; sourceTree = ""; }; - 4B0E04F91FC9FA3100F43484 /* 9918.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = 9918.cpp; sourceTree = ""; }; 4B0E61051FF34737002A9DBD /* MSX.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MSX.cpp; path = Parsers/MSX.cpp; sourceTree = ""; }; 4B0E61061FF34737002A9DBD /* MSX.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = MSX.hpp; path = Parsers/MSX.hpp; sourceTree = ""; }; 4B0F1BB02602645900B85C66 /* StaticAnalyser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StaticAnalyser.cpp; sourceTree = ""; }; @@ -1247,6 +1246,7 @@ 4B228CDA24DA41880077EF25 /* ScanTarget.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = ScanTarget.metal; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.metal; }; 4B24095A1C45DF85004DA684 /* Stepper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Stepper.hpp; sourceTree = ""; }; 4B2530F3244E6773007980BF /* fm.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = fm.json; sourceTree = ""; }; + 4B262BFF29691F55002EC0F7 /* PersonalityTraits.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PersonalityTraits.hpp; sourceTree = ""; }; 4B2A332C1DB86821002876E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/OricOptions.xib"; sourceTree = SOURCE_ROOT; }; 4B2A53901D117D36003C6002 /* CSAudioQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSAudioQueue.h; sourceTree = ""; }; 4B2A53911D117D36003C6002 /* CSAudioQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSAudioQueue.m; sourceTree = ""; }; @@ -1317,6 +1317,11 @@ 4B3FCC3F201EC24200960631 /* MultiMachine.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MultiMachine.cpp; sourceTree = ""; }; 4B3FE75C1F3CF68B00448EE4 /* CPM.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CPM.cpp; path = Parsers/CPM.cpp; sourceTree = ""; }; 4B3FE75D1F3CF68B00448EE4 /* CPM.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = CPM.hpp; path = Parsers/CPM.hpp; sourceTree = ""; }; + 4B43983829620FB1006B0BFC /* 9918.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = 9918.cpp; sourceTree = ""; }; + 4B43983C29621024006B0BFC /* ClockConverter.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ClockConverter.hpp; sourceTree = ""; }; + 4B43983E29628538006B0BFC /* Fetch.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Fetch.hpp; sourceTree = ""; }; + 4B43983F2967459B006B0BFC /* Draw.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Draw.hpp; sourceTree = ""; }; + 4B43984129674943006B0BFC /* BitReverse.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = BitReverse.hpp; sourceTree = ""; }; 4B448E7F1F1C45A00009ABD6 /* TZX.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TZX.cpp; sourceTree = ""; }; 4B448E801F1C45A00009ABD6 /* TZX.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TZX.hpp; sourceTree = ""; }; 4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PulseQueuedTape.cpp; sourceTree = ""; }; @@ -2398,7 +2403,6 @@ 4B0E04F71FC9F2C800F43484 /* 9918 */ = { isa = PBXGroup; children = ( - 4B0E04F91FC9FA3100F43484 /* 9918.cpp */, 4B0E04F81FC9FA3000F43484 /* 9918.hpp */, 4BD388431FE34E060042B588 /* Implementation */, ); @@ -3288,6 +3292,7 @@ 4B7BA03C23D55E7900B98D9E /* Numeric */ = { isa = PBXGroup; children = ( + 4B43984129674943006B0BFC /* BitReverse.hpp */, 4BD155312716362A00410C6E /* BitSpread.hpp */, 4B7BA03E23D55E7900B98D9E /* CRC.hpp */, 4B7BA03F23D55E7900B98D9E /* LFSR.hpp */, @@ -4733,7 +4738,12 @@ 4BD388431FE34E060042B588 /* Implementation */ = { isa = PBXGroup; children = ( + 4B43983829620FB1006B0BFC /* 9918.cpp */, 4BD388411FE34E010042B588 /* 9918Base.hpp */, + 4B43983C29621024006B0BFC /* ClockConverter.hpp */, + 4B43983F2967459B006B0BFC /* Draw.hpp */, + 4B43983E29628538006B0BFC /* Fetch.hpp */, + 4B262BFF29691F55002EC0F7 /* PersonalityTraits.hpp */, ); path = Implementation; sourceTree = ""; @@ -5519,7 +5529,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4B0E04FB1FC9FA3100F43484 /* 9918.cpp in Sources */, 4B1B88C9202E469400B67DFF /* MultiJoystickMachine.cpp in Sources */, 4BCE1DF225D4C3FA00AE7A2B /* Bus.cpp in Sources */, 4BC080DA26A25ADA00D03FD8 /* Amiga.cpp in Sources */, @@ -5529,6 +5538,7 @@ 4B1B5900246E19FD009C171E /* State.cpp in Sources */, 4BC57CDA2436A62900FBC404 /* State.cpp in Sources */, 4B055ACB1FAE9AFB0060FFFF /* SerialBus.cpp in Sources */, + 4B43983B29620FC9006B0BFC /* 9918.cpp in Sources */, 4B8318B122D3E53A006DB630 /* DiskIICard.cpp in Sources */, 4B055A9B1FAE85DA0060FFFF /* AcornADF.cpp in Sources */, 4B0E04F11FC9EA9500F43484 /* MSX.cpp in Sources */, @@ -5821,6 +5831,7 @@ 4BC080CA26A238CC00D03FD8 /* AmigaADF.cpp in Sources */, 4B1A1B1E27320FBC00119335 /* Disk.cpp in Sources */, 4B92E26A234AE35100CD6D1B /* MFP68901.cpp in Sources */, + 4B43983929620FC7006B0BFC /* 9918.cpp in Sources */, 4B051C97266EF5F600CA44E8 /* CSAppleII.mm in Sources */, 4B0ACC2A23775819008902D0 /* Video.cpp in Sources */, 4B54C0BF1F8D8F450050900F /* Keyboard.cpp in Sources */, @@ -5927,7 +5938,6 @@ 4B17B58B20A8A9D9007CCA8F /* StringSerialiser.cpp in Sources */, 4B2E2D9D1C3A070400138695 /* Electron.cpp in Sources */, 4B051CA826781D6500CA44E8 /* StaticAnalyser.cpp in Sources */, - 4B0E04FA1FC9FA3100F43484 /* 9918.cpp in Sources */, 4B69FB3D1C4D908A00B5F0AA /* Tape.cpp in Sources */, 4B4518841F75E91A00926311 /* UnformattedTrack.cpp in Sources */, 4B65086022F4CF8D009C1100 /* Keyboard.cpp in Sources */, @@ -6123,6 +6133,7 @@ 4BE34438238389E10058E78F /* AtariSTVideoTests.mm in Sources */, 4BEF6AAC1D35D1C400E73575 /* DPLLTests.swift in Sources */, 4BE76CF922641ED400ACD6FA /* QLTests.mm in Sources */, + 4B43983A29620FC8006B0BFC /* 9918.cpp in Sources */, 4B778F0923A5EC150000D260 /* OricTAP.cpp in Sources */, 4B3BA0CF1D318B44005DD7A7 /* MOS6522Bridge.mm in Sources */, 4B778F6023A5F3460000D260 /* Disk.cpp in Sources */, @@ -6138,7 +6149,6 @@ 4B0DA67D282DCDF300C12F17 /* Instruction.cpp in Sources */, 4BFCA12B1ECBE7C400AC40C1 /* ZexallTests.swift in Sources */, 4B778F2223A5EDDD0000D260 /* PulseQueuedTape.cpp in Sources */, - 4B778EF123A5D6B50000D260 /* 9918.cpp in Sources */, 4B051CB3267D3FF800CA44E8 /* EnterpriseNickTests.mm in Sources */, 4B9D0C4D22C7DA1A00DE1AD3 /* 68000ControlFlowTests.mm in Sources */, 4BB2A9AF1E13367E001A5C23 /* CRCTests.mm in Sources */, diff --git a/OSBindings/Mac/Clock SignalTests/MasterSystemVDPTests.mm b/OSBindings/Mac/Clock SignalTests/MasterSystemVDPTests.mm index a79ab8e8a..62a306c7e 100644 --- a/OSBindings/Mac/Clock SignalTests/MasterSystemVDPTests.mm +++ b/OSBindings/Mac/Clock SignalTests/MasterSystemVDPTests.mm @@ -19,8 +19,10 @@ [super setUp]; } +using VDP = TI::TMS::TMS9918; + - (void)testLineInterrupt { - TI::TMS::TMS9918 vdp(TI::TMS::Personality::SMSVDP); + VDP vdp; // Disable end-of-frame interrupts, enable line interrupts. vdp.write(1, 0x00); @@ -63,7 +65,7 @@ } - (void)testFirstLineInterrupt { - TI::TMS::TMS9918 vdp(TI::TMS::Personality::SMSVDP); + VDP vdp; // Disable end-of-frame interrupts, enable line interrupts, set an interrupt to occur every line. vdp.write(1, 0x00); @@ -96,7 +98,7 @@ } - (void)testInterruptPrediction { - TI::TMS::TMS9918 vdp(TI::TMS::Personality::SMSVDP); + VDP vdp; for(int c = 0; c < 256; ++c) { for(int with_eof = (c < 192) ? 0 : 1; with_eof < 2; ++with_eof) { @@ -144,7 +146,7 @@ } - (void)testTimeUntilLine { - TI::TMS::TMS9918 vdp(TI::TMS::Personality::SMSVDP); + VDP vdp; auto time_until_line = vdp.get_time_until_line(-1).as_integral(); for(int c = 0; c < 262*228*5; ++c) { diff --git a/OSBindings/Qt/clksignal.pro b/OSBindings/Qt/clksignal.pro index 160673b94..a3c341a97 100644 --- a/OSBindings/Qt/clksignal.pro +++ b/OSBindings/Qt/clksignal.pro @@ -65,6 +65,7 @@ SOURCES += \ $$SRC/Components/8272/*.cpp \ $$SRC/Components/8530/*.cpp \ $$SRC/Components/9918/*.cpp \ + $$SRC/Components/9918/Implementation/*.cpp \ $$SRC/Components/AudioToggle/*.cpp \ $$SRC/Components/AY38910/*.cpp \ $$SRC/Components/DiskII/*.cpp \