diff --git a/Analyser/Machines.hpp b/Analyser/Machines.hpp index 679e8cb5d..fde24ec46 100644 --- a/Analyser/Machines.hpp +++ b/Analyser/Machines.hpp @@ -17,6 +17,7 @@ enum class Machine { Atari2600, ColecoVision, Electron, + MasterSystem, MSX, Oric, Vic20, diff --git a/Analyser/Static/Coleco/StaticAnalyser.hpp b/Analyser/Static/Coleco/StaticAnalyser.hpp index 77de5abf0..0101c8c71 100644 --- a/Analyser/Static/Coleco/StaticAnalyser.hpp +++ b/Analyser/Static/Coleco/StaticAnalyser.hpp @@ -23,5 +23,4 @@ TargetList GetTargets(const Media &media, const std::string &file_name, TargetPl } } - #endif /* StaticAnalyser_hpp */ diff --git a/Analyser/Static/Sega/StaticAnalyser.cpp b/Analyser/Static/Sega/StaticAnalyser.cpp new file mode 100644 index 000000000..c8d733c27 --- /dev/null +++ b/Analyser/Static/Sega/StaticAnalyser.cpp @@ -0,0 +1,32 @@ +// +// StaticAnalyser.cpp +// Clock Signal +// +// Created by Thomas Harte on 20/09/2018. +// Copyright © 2018 Thomas Harte. All rights reserved. +// + +#include "StaticAnalyser.hpp" + +#include "Target.hpp" + +Analyser::Static::TargetList Analyser::Static::Sega::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) { + TargetList targets; + std::unique_ptr target(new Target); + + target->machine = Machine::MasterSystem; + + // Files named .sg are treated as for the SG1000; otherwise assume a Master System. + if(file_name.size() >= 2 && *(file_name.end() - 2) == 's' && *(file_name.end() - 1) == 'g') { + target->model = Target::Model::SG1000; + } else { + target->model = Target::Model::MasterSystem; + } + + target->media.cartridges = media.cartridges; + + if(!target->media.empty()) + targets.push_back(std::move(target)); + + return targets; +} diff --git a/Analyser/Static/Sega/StaticAnalyser.hpp b/Analyser/Static/Sega/StaticAnalyser.hpp new file mode 100644 index 000000000..127e2cd8d --- /dev/null +++ b/Analyser/Static/Sega/StaticAnalyser.hpp @@ -0,0 +1,26 @@ +// +// StaticAnalyser.hpp +// Clock Signal +// +// Created by Thomas Harte on 20/09/2018. +// Copyright © 2018 Thomas Harte. All rights reserved. +// + +#ifndef StaticAnalyser_Sega_StaticAnalyser_hpp +#define StaticAnalyser_Sega_StaticAnalyser_hpp + +#include "../StaticAnalyser.hpp" +#include "../../../Storage/TargetPlatforms.hpp" +#include + +namespace Analyser { +namespace Static { +namespace Sega { + +TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); + +} +} +} + +#endif /* StaticAnalyser_hpp */ diff --git a/Analyser/Static/Sega/Target.hpp b/Analyser/Static/Sega/Target.hpp new file mode 100644 index 000000000..391b0e159 --- /dev/null +++ b/Analyser/Static/Sega/Target.hpp @@ -0,0 +1,29 @@ +// +// Target.hpp +// Clock Signal +// +// Created by Thomas Harte on 23/09/2018. +// Copyright © 2018 Thomas Harte. All rights reserved. +// + +#ifndef Analyser_Static_Sega_Target_h +#define Analyser_Static_Sega_Target_h + +namespace Analyser { +namespace Static { +namespace Sega { + +struct Target: public ::Analyser::Static::Target { + enum class Model { + MasterSystem, + SG1000 + }; + + Model model = Model::MasterSystem; +}; + +} +} +} + +#endif /* Analyser_Static_Sega_Target_h */ diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp index 5351ccecb..a65e9582b 100644 --- a/Analyser/Static/StaticAnalyser.cpp +++ b/Analyser/Static/StaticAnalyser.cpp @@ -23,6 +23,7 @@ #include "DiskII/StaticAnalyser.hpp" #include "MSX/StaticAnalyser.hpp" #include "Oric/StaticAnalyser.hpp" +#include "Sega/StaticAnalyser.hpp" #include "ZX8081/StaticAnalyser.hpp" // Cartridges @@ -129,6 +130,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: result.cartridges, Cartridge::BinaryDump, TargetPlatform::AcornElectron | TargetPlatform::ColecoVision | TargetPlatform::MSX) // ROM + Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SG + Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS Format("ssd", result.disks, Disk::DiskImageHolder, TargetPlatform::Acorn) // SSD Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore) Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric) @@ -170,6 +173,7 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) { if(potential_platforms & TargetPlatform::ColecoVision) Append(Coleco); if(potential_platforms & TargetPlatform::Commodore) Append(Commodore); if(potential_platforms & TargetPlatform::DiskII) Append(DiskII); + if(potential_platforms & TargetPlatform::Sega) Append(Sega); if(potential_platforms & TargetPlatform::MSX) Append(MSX); if(potential_platforms & TargetPlatform::Oric) Append(Oric); if(potential_platforms & TargetPlatform::ZX8081) Append(ZX8081); diff --git a/Components/9918/9918.cpp b/Components/9918/9918.cpp index c64bbac72..e4ae897a5 100644 --- a/Components/9918/9918.cpp +++ b/Components/9918/9918.cpp @@ -11,44 +11,12 @@ #include #include -using namespace TI; +using namespace TI::TMS; namespace { -const 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; -} - -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), - - 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) -}; - const uint8_t StatusInterrupt = 0x80; -const uint8_t StatusFifthSprite = 0x40; +const uint8_t StatusSpriteOverflow = 0x40; const int StatusSpriteCollisionShift = 5; const uint8_t StatusSpriteCollision = 0x20; @@ -72,23 +40,46 @@ struct ReverseTable { } } reverse_table; -// Bits are reversed in the internal mode value; they're stored -// in the order M1 M2 M3. Hence the definitions below. -enum ScreenMode { - Text = 4, - MultiColour = 2, - ColouredText = 0, - Graphics = 1 -}; - } -TMS9918Base::TMS9918Base() : +Base::Base(Personality p) : + personality_(p), // 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. - crt_(new Outputs::CRT::CRT(1365, 4, Outputs::CRT::DisplayType::NTSC60, 4)) {} + crt_(new Outputs::CRT::CRT(1365, 4, Outputs::CRT::DisplayType::NTSC60, 4)) { -TMS9918::TMS9918(Personality p) { + switch(p) { + case TI::TMS::TMS9918A: + case TI::TMS::SMSVDP: + 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 = 65; + + mode_timing_.end_of_frame_interrupt_position.column = 63; + mode_timing_.end_of_frame_interrupt_position.row = 193; + } + + // Establish that output is 10 cycles after values have been read. + // This is definitely correct for the TMS; more research may be + // necessary for the other implemented VDPs. + read_pointer_.row = 0; + read_pointer_.column = 0; + write_pointer_.row = 0; + write_pointer_.column = 10; // i.e. 10 cycles ahead of the read pointer. +} + +TMS9918::TMS9918(Personality p): + Base(p) { // Unimaginatively, this class just passes RGB through to the shader. Investigation is needed // into whether there's a more natural form. crt_->set_rgb_sampling_function( @@ -111,66 +102,40 @@ Outputs::CRT::CRT *TMS9918::get_crt() { return crt_.get(); } -void TMS9918Base::test_sprite(int sprite_number, int screen_row) { - if(!(status_ & StatusFifthSprite)) { - status_ = static_cast((status_ & ~31) | sprite_number); +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; } - if(sprites_stopped_) - return; - - const int sprite_position = ram_[sprite_attribute_table_address_ + (sprite_number << 2)]; - // A sprite Y of 208 means "don't scan the list any further". - if(sprite_position == 208) { - sprites_stopped_ = true; - return; - } - - const int sprite_row = (screen_row - sprite_position)&255; - if(sprite_row < 0 || sprite_row >= sprite_height_) return; - - const int active_sprite_slot = sprite_sets_[active_sprite_set_].active_sprite_slot; - if(active_sprite_slot == 4) { - status_ |= StatusFifthSprite; - return; - } - - SpriteSet::ActiveSprite &sprite = sprite_sets_[active_sprite_set_].active_sprites[active_sprite_slot]; - sprite.index = sprite_number; - sprite.row = sprite_row >> (sprites_magnified_ ? 1 : 0); - sprite_sets_[active_sprite_set_].active_sprite_slot++; } -void TMS9918Base::get_sprite_contents(int field, int cycles_left, int screen_row) { - int sprite_id = field / 6; - field %= 6; - - while(true) { - const int cycles_in_sprite = std::min(cycles_left, 6 - field); - cycles_left -= cycles_in_sprite; - const int final_field = cycles_in_sprite + field; - - assert(sprite_id < 4); - SpriteSet::ActiveSprite &sprite = sprite_sets_[active_sprite_set_].active_sprites[sprite_id]; - - if(field < 4) { - std::memcpy( - &sprite.info[field], - &ram_[sprite_attribute_table_address_ + (sprite.index << 2) + field], - static_cast(std::min(4, final_field) - field)); - } - - field = std::min(4, final_field); - const int sprite_offset = sprite.info[2] & ~(sprites_16x16_ ? 3 : 0); - const int sprite_address = sprite_generator_table_address_ + (sprite_offset << 3) + sprite.row; // TODO: recalclate sprite.row from screen_row (?) - while(field < final_field) { - sprite.image[field - 4] = ram_[sprite_address + ((field - 4) << 4)]; - field++; - } - - if(!cycles_left) return; - field = 0; - sprite_id++; +void Base::posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_position, int screen_row) { + if(!(status_ & StatusSpriteOverflow)) { + status_ = static_cast((status_ & ~0x1f) | (sprite_number & 0x1f)); } + if(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 == 208) { + 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) { @@ -179,10 +144,6 @@ void TMS9918::run_for(const HalfCycles cycles) { // PAL output is 313 lines total. NTSC output is 262 lines total. // Interrupt is signalled upon entering the lower border. - // Keep a count of cycles separate from internal counts to avoid - // potential errors mapping back and forth. - half_cycles_into_frame_ = (half_cycles_into_frame_ + cycles) % HalfCycles(frame_lines_ * 228 * 2); - // 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. @@ -191,435 +152,265 @@ void TMS9918::run_for(const HalfCycles cycles) { int_cycles >>= 2; if(!int_cycles) return; - while(int_cycles) { - // Determine how much time has passed in the remainder of this line, and proceed. - int cycles_left = std::min(342 - column_, int_cycles); + // 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) { + LineBufferPointer backup = read_pointer_; + + 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]; - // ------------------------------------ - // Potentially perform a memory access. - // ------------------------------------ - if(queued_access_ != MemoryAccess::None) { - int time_until_access_slot = 0; - switch(line_mode_) { - case LineMode::Refresh: - if(column_ < 53 || column_ >= 307) time_until_access_slot = column_&1; - else time_until_access_slot = 3 - ((column_ - 53)&3); - // i.e. 53 -> 3, 52 -> 2, 51 -> 1, 50 -> 0, etc - break; - case LineMode::Text: - if(column_ < 59 || column_ >= 299) time_until_access_slot = column_&1; - else time_until_access_slot = 5 - ((column_ + 3)%6); - // i.e. 59 -> 3, 60 -> 2, 61 -> 1, etc - break; - case LineMode::Character: - if(column_ < 9) time_until_access_slot = column_&1; - else if(column_ < 30) time_until_access_slot = 30 - column_; - else if(column_ < 37) time_until_access_slot = column_&1; - else if(column_ < 311) time_until_access_slot = 31 - ((column_ + 7)&31); - // i.e. 53 -> 3, 54 -> 2, 55 -> 1, 56 -> 0, 57 -> 31, etc - else if(column_ < 313) time_until_access_slot = column_&1; - else time_until_access_slot = 342 - column_; - break; + // --------------------------------------- + // Latch scrolling position, if necessary. + // --------------------------------------- + if(write_pointer_.column < 61 && end_column >= 61) { + if(!write_pointer_.row) { + master_system_.latched_vertical_scroll = master_system_.vertical_scroll; + } + line_buffer.latched_horizontal_scroll = master_system_.horizontal_scroll; } - if(cycles_left >= time_until_access_slot) { - if(queued_access_ == MemoryAccess::Write) { - ram_[ram_pointer_ & 16383] = read_ahead_buffer_; + + + // ------------------------ + // 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; + } + } + + + assert(backup.row == read_pointer_.row && backup.column == read_pointer_.column); + backup = write_pointer_; + + + if(read_cycles_pool) { + // Determine how much time has passed in the remainder of this line, and proceed. + const int read_cycles = std::min(342 - read_pointer_.column, read_cycles_pool); + const int end_column = read_pointer_.column + read_cycles; + LineBuffer &line_buffer = line_buffers_[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;\ + }\ + } + + 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 { - read_ahead_buffer_ = ram_[ram_pointer_ & 16383]; + // Right border. + intersect(0, 15, output_border(end - start)); + + // Blanking region; total length is 58 cycles, + // and 58+15 = 73. So output the lot when the + // cursor passes 73. + if(read_pointer_.column < 73 && end_column >= 73) { + crt_->output_blank(8*4); + crt_->output_sync(26*4); + crt_->output_blank(2*4); + crt_->output_default_colour_burst(14*4); + crt_->output_blank(8*4); + } + + // Border colour for the rest of the line. + intersect(73, 342, output_border(end - start)); } - ram_pointer_++; - queued_access_ = MemoryAccess::None; - } - } + } else { + // Right border. + intersect(0, 15, output_border(end - start)); + // 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. + intersect(73, line_buffer.first_pixel_output_column, output_border(end - start)); - column_ += cycles_left; // column_ is now the column that has been reached in this line. - int_cycles -= cycles_left; // Count down duration to run for. - - - - // ------------------------------ - // Perform video memory accesses. - // ------------------------------ - if(((row_ < 192) || (row_ == frame_lines_-1)) && !blank_screen_) { - const int sprite_row = (row_ < 192) ? row_ : -1; - const int access_slot = column_ >> 1; // There are only 171 available memory accesses per line. - switch(line_mode_) { - default: break; - - case LineMode::Text: - access_pointer_ = std::min(30, access_slot); - if(access_pointer_ >= 30 && access_pointer_ < 150) { - const int row_base = pattern_name_address_ + (row_ >> 3) * 40; - const int end = std::min(150, access_slot); - - // Pattern names are collected every third window starting from window 30. - const int pattern_names_start = (access_pointer_ - 30 + 2) / 3; - const int pattern_names_end = (end - 30 + 2) / 3; - std::memcpy(&pattern_names_[pattern_names_start], &ram_[row_base + pattern_names_start], static_cast(pattern_names_end - pattern_names_start)); - - // Patterns are collected every third window starting from window 32. - const int pattern_buffer_start = (access_pointer_ - 32 + 2) / 3; - const int pattern_buffer_end = (end - 32 + 2) / 3; - for(int column = pattern_buffer_start; column < pattern_buffer_end; ++column) { - pattern_buffer_[column] = ram_[pattern_generator_table_address_ + (pattern_names_[column] << 3) + (row_ & 7)]; - } - } - break; - - case LineMode::Character: - // Four access windows: no collection. - if(access_pointer_ < 5) - access_pointer_ = std::min(5, access_slot); - - // Then ten access windows are filled with collection of sprite 3 and 4 details. - if(access_pointer_ >= 5 && access_pointer_ < 15) { - int end = std::min(15, access_slot); - get_sprite_contents(access_pointer_ - 5 + 14, end - access_pointer_, sprite_row - 1); - access_pointer_ = std::min(15, access_slot); + // Pixel region. + intersect( + line_buffer.first_pixel_output_column, + line_buffer.next_border_column, + if(start == line_buffer.first_pixel_output_column) { + pixel_origin_ = pixel_target_ = reinterpret_cast( + crt_->allocate_write_area(static_cast(line_buffer.next_border_column - line_buffer.first_pixel_output_column)) + ); } - // Four more access windows: no collection. - if(access_pointer_ >= 15 && access_pointer_ < 19) { - access_pointer_ = std::min(19, access_slot); + if(pixel_target_) { + const int relative_start = start - line_buffer.first_pixel_output_column; + const int relative_end = end - line_buffer.first_pixel_output_column; + switch(line_buffer.line_mode) { + case LineMode::SMS: draw_sms(relative_start, relative_end); break; + case LineMode::Character: draw_tms_character(relative_start, relative_end); break; + case LineMode::Text: draw_tms_text(relative_start, relative_end); break; - // Start new sprite set if this is location 19. - if(access_pointer_ == 19) { - active_sprite_set_ ^= 1; - sprite_sets_[active_sprite_set_].active_sprite_slot = 0; - sprites_stopped_ = false; + case LineMode::Refresh: break; /* Dealt with elsewhere. */ } } - // Then eight access windows fetch the y position for the first eight sprites. - while(access_pointer_ < 27 && access_pointer_ < access_slot) { - test_sprite(access_pointer_ - 19, sprite_row); - access_pointer_++; + if(end == line_buffer.next_border_column) { + const unsigned int length = static_cast(line_buffer.next_border_column - line_buffer.first_pixel_output_column); + crt_->output_data(length * 4, length); + pixel_origin_ = pixel_target_ = nullptr; } + ); - // The next 128 access slots are video and sprite collection interleaved. - if(access_pointer_ >= 27 && access_pointer_ < 155) { - int end = std::min(155, access_slot); - - int row_base = pattern_name_address_; - int pattern_base = pattern_generator_table_address_; - int colour_base = colour_table_address_; - if(screen_mode_ == ScreenMode::Graphics) { - // If this is high resolution mode, allow the row number to affect the pattern and colour addresses. - pattern_base &= 0x2000 | ((row_ & 0xc0) << 5); - colour_base &= 0x2000 | ((row_ & 0xc0) << 5); - } - row_base += (row_ << 2)&~31; - - // Pattern names are collected every fourth window starting from window 27. - const int pattern_names_start = (access_pointer_ - 27 + 3) >> 2; - const int pattern_names_end = (end - 27 + 3) >> 2; - std::memcpy(&pattern_names_[pattern_names_start], &ram_[row_base + pattern_names_start], static_cast(pattern_names_end - pattern_names_start)); - - // Colours are collected every fourth window starting from window 29. - const int colours_start = (access_pointer_ - 29 + 3) >> 2; - const int colours_end = (end - 29 + 3) >> 2; - if(screen_mode_ != 1) { - for(int column = colours_start; column < colours_end; ++column) { - colour_buffer_[column] = ram_[colour_base + (pattern_names_[column] >> 3)]; - } - } else { - for(int column = colours_start; column < colours_end; ++column) { - colour_buffer_[column] = ram_[colour_base + (pattern_names_[column] << 3) + (row_ & 7)]; - } - } - - // Patterns are collected ever fourth window starting from window 30. - const int pattern_buffer_start = (access_pointer_ - 30 + 3) >> 2; - const int pattern_buffer_end = (end - 30 + 3) >> 2; - - // Multicolour mode uss a different function of row to pick bytes - const int row = (screen_mode_ != 2) ? (row_ & 7) : ((row_ >> 2) & 7); - for(int column = pattern_buffer_start; column < pattern_buffer_end; ++column) { - pattern_buffer_[column] = ram_[pattern_base + (pattern_names_[column] << 3) + row]; - } - - // Sprite slots occur in three quarters of ever fourth window starting from window 28. - const int sprite_start = (access_pointer_ - 28 + 3) >> 2; - const int sprite_end = (end - 28 + 3) >> 2; - for(int column = sprite_start; column < sprite_end; ++column) { - if(column&3) { - test_sprite(7 + column - (column >> 2), sprite_row); - } - } - - access_pointer_ = std::min(155, access_slot); - } - - // Two access windows: no collection. - if(access_pointer_ < 157) - access_pointer_ = std::min(157, access_slot); - - // Fourteen access windows: collect initial sprite information. - if(access_pointer_ >= 157 && access_pointer_ < 171) { - int end = std::min(171, access_slot); - get_sprite_contents(access_pointer_ - 157, end - access_pointer_, sprite_row); - access_pointer_ = std::min(171, access_slot); - } - break; - } - } - // -------------------------- - // End video memory accesses. - // -------------------------- - - - - // -------------------- - // Output video stream. - // -------------------- - if(row_ < 192 && !blank_screen_) { - // ---------------------- - // Output horizontal sync - // ---------------------- - if(!output_column_ && column_ >= 26) { - crt_->output_sync(13 * 4); - crt_->output_default_colour_burst(13 * 4); - output_column_ = 26; - } - - // ------------------- - // Output left border. - // ------------------- - if(output_column_ >= 26) { - int pixels_end = std::min(first_pixel_column_, column_); - if(output_column_ < pixels_end) { - output_border(pixels_end - output_column_); - output_column_ = pixels_end; - - // Grab a pointer for drawing pixels to, if the moment has arrived. - if(pixels_end == first_pixel_column_) { - pixel_base_ = pixel_target_ = reinterpret_cast(crt_->allocate_write_area(static_cast(first_right_border_column_ - first_pixel_column_))); - } + // Additional right border, if called for. + if(line_buffer.next_border_column != 342) { + intersect(line_buffer.next_border_column, 342, output_border(end - start)); } } - // -------------- - // Output pixels. - // -------------- - if(output_column_ >= first_pixel_column_) { - int pixels_end = std::min(first_right_border_column_, column_); + #undef intersect - if(output_column_ < pixels_end) { - switch(line_mode_) { - default: break; - case LineMode::Text: { - const uint32_t colours[2] = { palette[background_colour_], palette[text_colour_] }; - const int shift = (output_column_ - first_pixel_column_) % 6; - int byte_column = (output_column_ - first_pixel_column_) / 6; - int pattern = reverse_table.map[pattern_buffer_[byte_column]] >> shift; - int pixels_left = pixels_end - output_column_; - 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; + // ------------- + // Advance time. + // ------------- + read_pointer_.column = end_column; + read_cycles_pool -= read_cycles; - if(!pixels_left) break; - length = std::min(6, pixels_left); - byte_column++; - pattern = reverse_table.map[pattern_buffer_[byte_column]]; - } - output_column_ = pixels_end; - } break; - - case LineMode::Character: { - // If this is the start of the visible area, seed sprite shifter positions. - SpriteSet &sprite_set = sprite_sets_[active_sprite_set_ ^ 1]; - if(output_column_ == first_pixel_column_) { - int c = sprite_set.active_sprite_slot; - while(c--) { - SpriteSet::ActiveSprite &sprite = sprite_set.active_sprites[c]; - sprite.shift_position = -sprite.info[1]; - if(sprite.info[3] & 0x80) { - sprite.shift_position += 32; - if(sprite.shift_position > 0 && !sprites_magnified_) - sprite.shift_position *= 2; - } - } - } - - // Paint the background tiles. - const int pixels_left = pixels_end - output_column_; - if(screen_mode_ == ScreenMode::MultiColour) { - int pixel_location = output_column_ - first_pixel_column_; - for(int c = 0; c < pixels_left; ++c) { - pixel_target_[c] = palette[ - (pattern_buffer_[(pixel_location + c) >> 3] >> (((pixel_location + c) & 4)^4)) & 15 - ]; - } - pixel_target_ += pixels_left; - } else { - const int shift = (output_column_ - first_pixel_column_) & 7; - int byte_column = (output_column_ - first_pixel_column_) >> 3; - - int length = std::min(pixels_left, 8 - shift); - - int pattern = reverse_table.map[pattern_buffer_[byte_column]] >> shift; - uint8_t colour = colour_buffer_[byte_column]; - 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[pattern_buffer_[byte_column]]; - colour = colour_buffer_[byte_column]; - 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(sprite_set.active_sprite_slot) { - int sprite_pixels_left = pixels_left; - const int shift_advance = sprites_magnified_ ? 1 : 2; - - static const uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; - static const int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; - - while(sprite_pixels_left--) { - // sprite_colour is the colour that's going to reach the display after sprite logic has been - // applied; by default assume that nothing is going to be drawn. - uint32_t sprite_colour = pixel_base_[output_column_ - first_pixel_column_]; - - // The sprite_mask is used to keep track of whether two sprites have both sought to output - // a pixel at the same location, and to feed that into the status register's sprite - // collision bit. - int sprite_mask = 0; - - int c = sprite_set.active_sprite_slot; - while(c--) { - SpriteSet::ActiveSprite &sprite = sprite_set.active_sprites[c]; - - if(sprite.shift_position < 0) { - sprite.shift_position++; - continue; - } else if(sprite.shift_position < 32) { - int mask = sprite.image[sprite.shift_position >> 4] << ((sprite.shift_position&15) >> 1); - mask = (mask >> 7) & 1; - - // Ignore the right half of whatever was collected if sprites are not in 16x16 mode. - if(sprite.shift_position < (sprites_16x16_ ? 32 : 16)) { - // If any previous sprite has been painted in this column and this sprite - // has this pixel set, set the sprite collision status bit. - status_ |= (mask & sprite_mask) << StatusSpriteCollisionShift; - sprite_mask |= mask; - - // Check that the sprite colour is not transparent - mask &= colour_masks[sprite.info[3]&15]; - sprite_colour = (sprite_colour & sprite_colour_selection_masks[mask^1]) | (palette[sprite.info[3]&15] & sprite_colour_selection_masks[mask]); - } - - sprite.shift_position += shift_advance; - } - } - - // Output whichever sprite colour was on top. - pixel_base_[output_column_ - first_pixel_column_] = sprite_colour; - output_column_++; - } - } - - output_column_ = pixels_end; - } break; - } - - if(output_column_ == first_right_border_column_) { - const unsigned int data_length = static_cast(first_right_border_column_ - first_pixel_column_); - crt_->output_data(data_length * 4, data_length); - pixel_target_ = nullptr; - } - } - } - - // -------------------- - // Output right border. - // -------------------- - if(output_column_ >= first_right_border_column_) { - output_border(column_ - output_column_); - output_column_ = column_; - } - } else if(row_ >= first_vsync_line_ && row_ < first_vsync_line_+3) { - // Vertical sync. - if(column_ == 342) { - crt_->output_sync(342 * 4); - } - } else { - // Blank. - if(!output_column_ && column_ >= 26) { - crt_->output_sync(13 * 4); - crt_->output_default_colour_burst(13 * 4); - output_column_ = 26; - } - if(output_column_ >= 26) { - output_border(column_ - output_column_); - output_column_ = column_; + if(read_pointer_.column == 342) { + read_pointer_.column = 0; + read_pointer_.row = (read_pointer_.row + 1) % mode_timing_.total_lines; } } - // ----------------- - // End video stream. - // ----------------- - - - // ----------------------------------- - // Prepare for next line, potentially. - // ----------------------------------- - if(column_ == 342) { - access_pointer_ = column_ = output_column_ = 0; - row_ = (row_ + 1) % frame_lines_; - if(row_ == 192) status_ |= StatusInterrupt; - - screen_mode_ = next_screen_mode_; - blank_screen_ = next_blank_screen_; - switch(screen_mode_) { - case ScreenMode::Text: - line_mode_ = LineMode::Text; - first_pixel_column_ = 69; - first_right_border_column_ = 309; - break; - default: - line_mode_ = LineMode::Character; - first_pixel_column_ = 63; - first_right_border_column_ = 319; - break; - } - if(blank_screen_ || (row_ >= 192 && row_ != frame_lines_-1)) line_mode_ = LineMode::Refresh; - } + assert(backup.row == write_pointer_.row && backup.column == write_pointer_.column); } } -void TMS9918Base::output_border(int cycles) { - pixel_target_ = reinterpret_cast(crt_->allocate_write_area(1)); - if(pixel_target_) *pixel_target_ = palette[background_colour_]; +void Base::output_border(int cycles) { + uint32_t *const pixel_target = reinterpret_cast(crt_->allocate_write_area(1)); + if(pixel_target) { + if(is_sega_vdp(personality_)) { + *pixel_target = master_system_.colour_ram[16 + background_colour_]; + } else { + *pixel_target = palette[background_colour_]; + } + } crt_->output_level(static_cast(cycles) * 4); } @@ -641,21 +432,50 @@ void TMS9918::set_register(int address, uint8_t value) { 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) | static_cast(value << 8); + write_phase_ = false; if(value & 0x80) { + switch(personality_) { + default: + value &= 0x7; + break; + case TI::TMS::SMSVDP: + if(value & 0x40) { + master_system_.cram_is_selected = true; + return; + } + value &= 0xf; + break; + } + // This is a write to a register. - switch(value & 7) { + switch(value) { case 0: - next_screen_mode_ = (next_screen_mode_ & 6) | ((low_write_ & 2) >> 1); + 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: - next_blank_screen_ = !(low_write_ & 0x40); + blank_display_ = !(low_write_ & 0x40); generate_interrupts_ = !!(low_write_ & 0x20); - next_screen_mode_ = (next_screen_mode_ & 1) | ((low_write_ & 0x18) >> 2); + mode1_enable_ = !!(low_write_ & 0x10); + mode3_enable_ = !!(low_write_ & 0x08); sprites_16x16_ = !!(low_write_ & 0x02); sprites_magnified_ = !!(low_write_ & 0x01); @@ -665,40 +485,104 @@ void TMS9918::set_register(int address, uint8_t value) { break; case 2: - pattern_name_address_ = static_cast((low_write_ & 0xf) << 10); + pattern_name_address_ = size_t((low_write_ & 0xf) << 10) | 0x3ff; break; case 3: - colour_table_address_ = static_cast(low_write_ << 6); + colour_table_address_ = size_t(low_write_ << 6) | 0x3f; break; case 4: - pattern_generator_table_address_ = static_cast((low_write_ & 0x07) << 11); + pattern_generator_table_address_ = size_t((low_write_ & 0x07) << 11) | 0x7ff; break; case 5: - sprite_attribute_table_address_ = static_cast((low_write_ & 0x7f) << 7); + sprite_attribute_table_address_ = size_t((low_write_ & 0x7f) << 7) | 0x7f; break; case 6: - sprite_generator_table_address_ = static_cast((low_write_ & 0x07) << 11); + sprite_generator_table_address_ = size_t((low_write_ & 0x07) << 11) | 0x7ff; 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_; +// printf("Set to %d at %d, %d\n", low_write_, row_, column_); + } + 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: +// printf("Unknown TMS write: %d to %d\n", low_write_, value); + break; } } else { - // This is a write to the RAM pointer. - ram_pointer_ = static_cast(low_write_ | (value << 8)); + // This is an access via the RAM pointer. if(!(value & 0x40)) { - // Officially a 'read' set, so perform lookahead. + // 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; } + master_system_.cram_is_selected = false; } } +uint8_t TMS9918::get_current_line() { + // Determine the row to return. + static const int row_change_position = 62; // This is the proper Master System value; substitute if any other VDPs turn out to have this functionality. + int source_row = + (write_pointer_.column < row_change_position) + ? (write_pointer_.row + mode_timing_.total_lines - 1)%mode_timing_.total_lines + : write_pointer_.row; + + // This assumes NTSC 192-line. TODO: other modes. + if(source_row >= 0xdb) source_row -= 6; +// printf("Current row: %d -> %d\n", row_, source_row); + + return static_cast(source_row); + +/* + TODO: Full proper sequence of current lines: + + NTSC 256x192 00-DA, D5-FF + NTSC 256x224 00-EA, E5-FF + NTSC 256x240 00-FF, 00-06 + PAL 256x192 00-F2, BA-FF + PAL 256x224 00-FF, 00-02, CA-FF + PAL 256x240 00-FF, 00-0A, D2-FF +*/ +} + +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::get_register(int address) { write_phase_ = false; @@ -712,19 +596,335 @@ uint8_t TMS9918::get_register(int address) { // Reads from address 1 get the status register. uint8_t result = status_; - status_ &= ~(StatusInterrupt | StatusFifthSprite | StatusSpriteCollision); + status_ &= ~(StatusInterrupt | StatusSpriteOverflow | StatusSpriteCollision); + line_interrupt_pending_ = false; return result; } - HalfCycles TMS9918::get_time_until_interrupt() { - if(!generate_interrupts_) return HalfCycles(-1); +HalfCycles Base::half_cycles_before_internal_cycles(int internal_cycles) { + return HalfCycles(((internal_cycles << 2) - cycles_error_) / 3); +} + +HalfCycles TMS9918::get_time_until_interrupt() { + if(!generate_interrupts_ && !enable_line_interrupts_) return HalfCycles(-1); if(get_interrupt_line()) return HalfCycles(0); - const int half_cycles_per_frame = frame_lines_ * 228 * 2; - int half_cycles_remaining = (192 * 228 * 2 + half_cycles_per_frame - half_cycles_into_frame_.as_int()) % half_cycles_per_frame; - return HalfCycles(half_cycles_remaining ? half_cycles_remaining : half_cycles_per_frame); + // Calculate the amount of time until the next end-of-frame interrupt. + const int frame_length = 342 * mode_timing_.total_lines; + const int time_until_frame_interrupt = + ( + ((mode_timing_.end_of_frame_interrupt_position.row * 342) + mode_timing_.end_of_frame_interrupt_position.column + frame_length) - + ((write_pointer_.row * 342) + write_pointer_.column) + ) % frame_length; + + if(!enable_line_interrupts_) return half_cycles_before_internal_cycles(time_until_frame_interrupt); + + // Calculate the row upon which the next line interrupt will occur; + int next_line_interrupt_row = -1; + + 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(write_pointer_.row+line_interrupt_counter <= mode_timing_.pixel_lines) { + next_line_interrupt_row = write_pointer_.row+line_interrupt_counter; + } else { + if(line_interrupt_target <= mode_timing_.pixel_lines) + next_line_interrupt_row = mode_timing_.total_lines + line_interrupt_target; + } + } + + // 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(-1); + } + + // Figure out the number of internal cycles until the next line interrupt, which is the amount + // of time to the next tick over and then next_line_interrupt_row - row_ lines further. + int local_cycles_until_next_tick = (mode_timing_.line_interrupt_position - write_pointer_.column + 342) % 342; + if(!local_cycles_until_next_tick) local_cycles_until_next_tick += 342; + const int local_cycles_until_line_interrupt = local_cycles_until_next_tick + (next_line_interrupt_row - write_pointer_.row) * 342; + + if(!generate_interrupts_) return half_cycles_before_internal_cycles(time_until_frame_interrupt); + + // Return whichever interrupt is closer. + return half_cycles_before_internal_cycles(std::min(local_cycles_until_line_interrupt, time_until_frame_interrupt)); } bool TMS9918::get_interrupt_line() { - return (status_ & StatusInterrupt) && generate_interrupts_; + return ((status_ & StatusInterrupt) && generate_interrupts_) || (enable_line_interrupts_ && line_interrupt_pending_); +} + +// MARK: - + +// if(sprite.shift_position > 0 && !sprites_magnified_) +// sprite.shift_position *= 2; + +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 + ]; + } + pixel_target_ += pixels_left; + } 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])); + + static const uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; + static const 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) { + 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. + for(int c = start; 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 4f2eff794..5dc552e67 100644 --- a/Components/9918/9918.hpp +++ b/Components/9918/9918.hpp @@ -17,6 +17,7 @@ #include namespace TI { +namespace TMS { /*! Provides emulation of the TMS9918a, TMS9928 and TMS9929. Likely in the future to be the @@ -29,12 +30,8 @@ namespace TI { 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 TMS9918Base { +class TMS9918: public Base { public: - enum Personality { - TMS9918A, // includes the 9928 and 9929; set TV standard and output device as desired. - }; - /*! Constructs an instance of the drive controller that behaves according to personality @c p. @param p The type of controller to emulate. @@ -66,6 +63,13 @@ class TMS9918: public TMS9918Base { /*! Gets a register value. */ uint8_t get_register(int address); + /*! Gets the current scan line; provided by the Master System only. */ + uint8_t get_current_line(); + + uint8_t get_latched_horizontal_counter(); + + void latch_horizontal_counter(); + /*! Returns the amount of time until get_interrupt_line would next return true if there are no interceding calls to set_register or get_register. @@ -81,6 +85,7 @@ class TMS9918: public TMS9918Base { bool get_interrupt_line(); }; -}; +} +} #endif /* TMS9918_hpp */ diff --git a/Components/9918/Implementation/9918Base.hpp b/Components/9918/Implementation/9918Base.hpp index 862c7807c..77add1839 100644 --- a/Components/9918/Implementation/9918Base.hpp +++ b/Components/9918/Implementation/9918Base.hpp @@ -12,90 +12,780 @@ #include "../../../Outputs/CRT/CRT.hpp" #include "../../../ClockReceiver/ClockReceiver.hpp" +#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, + GGVDP, +}; + +#define is_sega_vdp(x) x >= SMSVDP + +class Base { + public: + static const 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; + } -class TMS9918Base { protected: - TMS9918Base(); + // 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), + 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) + }; + + Base(Personality p); + + Personality personality_; std::unique_ptr crt_; - uint8_t ram_[16384]; + // Holds the contents of this VDP's connected DRAM. + std::vector ram_; + // 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; + // Holds the main status register. uint8_t status_ = 0; - bool write_phase_ = false; - uint8_t low_write_ = 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. - // The various register flags. - int next_screen_mode_ = 0, screen_mode_ = 0; - bool next_blank_screen_ = true, blank_screen_ = true; + // 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; - uint16_t pattern_name_address_ = 0; - uint16_t colour_table_address_ = 0; - uint16_t pattern_generator_table_address_ = 0; - uint16_t sprite_attribute_table_address_ = 0; - uint16_t sprite_generator_table_address_ = 0; + + size_t pattern_name_address_ = 0; // i.e. address of the tile map. + size_t colour_table_address_ = 0; // address of the colour map (if applicable). + size_t pattern_generator_table_address_ = 0; // address of the tile contents. + size_t sprite_attribute_table_address_ = 0; // address of the sprite list. + size_t sprite_generator_table_address_ = 0; // address of the sprite contents. uint8_t text_colour_ = 0; uint8_t background_colour_ = 0; - HalfCycles half_cycles_into_frame_; - int column_ = 0, row_ = 0, output_column_ = 0; + // This implementation of this chip officially accepts a 3.58Mhz clock, but runs + // internally at 5.37Mhz. The following two help to maintain a lossless conversion + // from the one to the other. int cycles_error_ = 0; - uint32_t *pixel_target_ = nullptr, *pixel_base_ = nullptr; + HalfCycles half_cycles_before_internal_cycles(int internal_cycles); + // Internal mechanisms for position tracking. + int latched_column_ = 0; + + // A helper function to output the current border colour for + // the number of cycles supplied. void output_border(int cycles); - // Vertical timing details. - int frame_lines_ = 262; - int first_vsync_line_ = 227; + // A struct to contain timing information for 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 { + int column = 4; + int row = 193; + } end_of_frame_interrupt_position; + int line_interrupt_position = -1; + + // Enables or disabled the recognition of 0xd0 as a sprite + // list terminator. + bool allow_sprite_terminator = true; + } mode_timing_; + + uint8_t line_interrupt_target = 0xff; + uint8_t line_interrupt_counter = 0; + bool enable_line_interrupts_ = false; + bool line_interrupt_pending_ = false; + + // 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_; - // Horizontal selections. enum class LineMode { - Text = 0, - Character = 1, - Refresh = 2 - } line_mode_ = LineMode::Text; - int first_pixel_column_, first_right_border_column_; + Text, + Character, + Refresh, + SMS + }; - uint8_t pattern_names_[40]; - uint8_t pattern_buffer_[40]; - uint8_t colour_buffer_[40]; + // 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; - struct SpriteSet { + // Holds the horizontal scroll position to apply to this line; + // of those VDPs currently implemented, affects the Master System only. + uint8_t latched_horizontal_scroll = 0; + + // The names array holds pattern names, as an offset into memory, and + // potentially flags also. + struct { + size_t offset; + uint8_t flags; + } names[40]; + + // The patterns array holds tile patterns, corresponding 1:1 with names. + // Four bytes per pattern is the maximum required by any + // currently-implemented VDP. + uint8_t patterns[40][4]; + + /* + Horizontal layout (on a 342-cycle clock): + + 15 cycles right border + 58 cycles blanking & sync + 13 cycles left border + + ... i.e. to cycle 86, then: + + border up to first_pixel_output_column; + pixels up to next_border_column; + border up to the end. + + e.g. standard 256-pixel modes will want to set + first_pixel_output_column = 86, next_border_column = 342. + */ + int first_pixel_output_column = 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; - int row = 0; + 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 info[4]; - uint8_t image[2]; + 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 shift_position = 0; - } active_sprites[4]; - int active_sprite_slot = 0; - } sprite_sets_[2]; - int active_sprite_set_ = 0; - bool sprites_stopped_ = false; + 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. - int access_pointer_ = 0; + void reset_sprite_collection(); + } line_buffers_[313]; + void posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_y, int screen_row); - inline void test_sprite(int sprite_number, int screen_row); - inline void get_sprite_contents(int start, int cycles, int screen_row); + // There is a delay between reading into the line buffer and outputting from there to the screen. That delay + // is observeable because reading time affects availability of memory accesses and therefore time in which + // to update sprites and tiles, but writing time affects when the palette is used and when the collision flag + // may end up being set. So the two processes are slightly decoupled. The end of reading one line may overlap + // with the beginning of writing the next, hence the two separate line buffers. + struct LineBufferPointer { + int row, column; + } read_pointer_, write_pointer_; + + // Extra information that affects the Master System output mode. + struct { + // 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; + } 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_.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_.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; + } + + void do_external_slot() { + switch(queued_access_) { + default: return; + + case MemoryAccess::Write: + if(master_system_.cram_is_selected) { + master_system_.colour_ram[ram_pointer_ & 0x1f] = palette_pack( + static_cast(((read_ahead_buffer_ >> 0) & 3) * 255 / 3), + static_cast(((read_ahead_buffer_ >> 2) & 3) * 255 / 3), + static_cast(((read_ahead_buffer_ >> 4) & 3) * 255 / 3) + ); + } else { + ram_[ram_pointer_ & 16383] = read_ahead_buffer_; + } + break; + case MemoryAccess::Read: + read_ahead_buffer_ = ram_[ram_pointer_ & 16383]; + break; + } + ++ram_pointer_; + queued_access_ = MemoryAccess::None; + } + +/* + 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;\ + case n + +#define external_slot(n) \ + slot(n): do_external_slot(); + +#define external_slots_2(n) \ + external_slot(n); \ + external_slot(n+1); + +#define external_slots_4(n) \ + external_slots_2(n); \ + external_slots_2(n+2); + +#define external_slots_8(n) \ + external_slots_4(n); \ + external_slots_4(n+4); + +#define external_slots_16(n) \ + external_slots_8(n); \ + external_slots_8(n+8); + +#define external_slots_32(n) \ + external_slots_16(n); \ + external_slots_16(n+16); + + +/*********************************************** + TMS9918 Fetching Code +************************************************/ + + template void fetch_tms_refresh(int start, int end) { +#define refresh(location) \ + 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 | static_cast(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];\ + } + +#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(); + 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_[\ + sprite_attribute_table_address_ & size_t(0x3f80 | (line_buffer.active_sprites[sprite].index << 1))\ + ] - (master_system_.shift_sprites_8px_left ? 8 : 0); \ + const uint8_t name = ram_[\ + sprite_attribute_table_address_ & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 1))\ + ] & (sprites_16x16_ ? ~1 : ~0);\ + const size_t graphic_location = sprite_generator_table_address_ & size_t(0x2000 | (name << 5) | (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_[sprite_attribute_table_address_ & (sprite | 0x3f00)], write_pointer_.row); \ + posit_sprite(sprite_selection_buffer, sprite+1, ram_[sprite_attribute_table_address_ & ((sprite + 1) | 0x3f00)], write_pointer_.row); \ + +#define fetch_tile_name(column, row_info) {\ + const size_t scrolled_column = (column - horizontal_offset) & 0x1f;\ + const size_t address = row_info.pattern_address_base + (scrolled_column << 1); \ + line_buffer.names[column].flags = ram_[address+1]; \ + line_buffer.names[column].offset = static_cast( \ + (((line_buffer.names[column].flags&1) << 8) | ram_[address]) << 5 \ + ) + row_info.sub_row[(line_buffer.names[column].flags&4) >> 2]; \ + } + +#define fetch_tile(column) \ + 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; + + // 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) % 224; + struct RowInfo { + size_t pattern_address_base; + size_t sub_row[2]; + }; + const RowInfo scrolled_row_info = { + pattern_name_address_ & static_cast(((scrolled_row & ~7) << 3) | 0x3800), + {static_cast((scrolled_row & 7) << 2), 28 ^ static_cast((scrolled_row & 7) << 2)} + }; + RowInfo row_info; + if(master_system_.vertical_scroll_lock) { + row_info.pattern_address_base = pattern_name_address_ & static_cast(((write_pointer_.row & ~7) << 3) | 0x3800); + row_info.sub_row[0] = size_t((write_pointer_.row & 7) << 2); + row_info.sub_row[1] = 28 ^ size_t((write_pointer_.row & 7) << 2); + } else row_info = scrolled_row_info; + + // ... and do the actual fetching, which follows this routine: + switch(start) { + default: assert(false); + + 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(); + 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; + void draw_tms_character(int start, int end); + void draw_tms_text(int start, int end); + void draw_sms(int start, int end); }; +} } #endif /* TMS9918Base_hpp */ diff --git a/Machines/ColecoVision/ColecoVision.cpp b/Machines/ColecoVision/ColecoVision.cpp index fbcd3ebb6..5f071307e 100644 --- a/Machines/ColecoVision/ColecoVision.cpp +++ b/Machines/ColecoVision/ColecoVision.cpp @@ -100,7 +100,7 @@ class Joystick: public Inputs::ConcreteJoystick { private: uint8_t direction_ = 0xff; - uint8_t keypad_ = 0xff; + uint8_t keypad_ = 0x7f; }; class ConcreteMachine: @@ -141,10 +141,19 @@ class ConcreteMachine: cartridge_address_limit_ = static_cast(0x8000 + cartridge_.size() - 1); if(cartridge_.size() > 32768) { + // Ensure the cartrige is a multiple of 16kb in size, as that won't + // be checked when paging. + const size_t extension = (16384 - cartridge_.size() & 16383) % 16384; + cartridge_.resize(cartridge_.size() + extension); + cartridge_pages_[0] = &cartridge_[cartridge_.size() - 16384]; cartridge_pages_[1] = cartridge_.data(); is_megacart_ = true; } else { + // Ensure at least 32kb is allocated to the cartrige so that + // reads are never out of bounds. + cartridge_.resize(32768); + cartridge_pages_[0] = cartridge_.data(); cartridge_pages_[1] = cartridge_.data() + 16384; is_megacart_ = false; @@ -161,7 +170,7 @@ class ConcreteMachine: } void setup_output(float aspect_ratio) override { - vdp_.reset(new TI::TMS9918(TI::TMS9918::TMS9918A)); + vdp_.reset(new TI::TMS::TMS9918(TI::TMS::TMS9918A)); get_crt()->set_video_signal(Outputs::CRT::VideoSignal::Composite); } @@ -197,131 +206,134 @@ class ConcreteMachine: time_since_vdp_update_ += length; time_since_sn76489_update_ += length; - uint16_t address = cycle.address ? *cycle.address : 0x0000; - switch(cycle.operation) { - case CPU::Z80::PartialMachineCycle::ReadOpcode: - if(!address) pc_zero_accesses_++; - case CPU::Z80::PartialMachineCycle::Read: - if(address < 0x2000) { - if(super_game_module_.replace_bios) { + // Act only if necessary. + if(cycle.is_terminal()) { + uint16_t address = cycle.address ? *cycle.address : 0x0000; + switch(cycle.operation) { + case CPU::Z80::PartialMachineCycle::ReadOpcode: + if(!address) pc_zero_accesses_++; + case CPU::Z80::PartialMachineCycle::Read: + if(address < 0x2000) { + if(super_game_module_.replace_bios) { + *cycle.value = super_game_module_.ram[address]; + } else { + *cycle.value = bios_[address]; + } + } else if(super_game_module_.replace_ram && address < 0x8000) { *cycle.value = super_game_module_.ram[address]; + } else if(address >= 0x6000 && address < 0x8000) { + *cycle.value = ram_[address & 1023]; + } else if(address >= 0x8000 && address <= cartridge_address_limit_) { + if(is_megacart_ && address >= 0xffc0) { + page_megacart(address); + } + *cycle.value = cartridge_pages_[(address >> 14)&1][address&0x3fff]; } else { - *cycle.value = bios_[address]; + *cycle.value = 0xff; } - } else if(super_game_module_.replace_ram && address < 0x8000) { - *cycle.value = super_game_module_.ram[address]; - } else if(address >= 0x6000 && address < 0x8000) { - *cycle.value = ram_[address & 1023]; - } else if(address >= 0x8000 && address <= cartridge_address_limit_) { - if(is_megacart_ && address >= 0xffc0) { + break; + + case CPU::Z80::PartialMachineCycle::Write: + if(super_game_module_.replace_bios && address < 0x2000) { + super_game_module_.ram[address] = *cycle.value; + } else if(super_game_module_.replace_ram && address >= 0x2000 && address < 0x8000) { + super_game_module_.ram[address] = *cycle.value; + } else if(address >= 0x6000 && address < 0x8000) { + ram_[address & 1023] = *cycle.value; + } else if(is_megacart_ && address >= 0xffc0) { page_megacart(address); } - *cycle.value = cartridge_pages_[(address >> 14)&1][address&0x3fff]; - } else { - *cycle.value = 0xff; - } - break; + break; - case CPU::Z80::PartialMachineCycle::Write: - if(super_game_module_.replace_bios && address < 0x2000) { - super_game_module_.ram[address] = *cycle.value; - } else if(super_game_module_.replace_ram && address >= 0x2000 && address < 0x8000) { - super_game_module_.ram[address] = *cycle.value; - } else if(address >= 0x6000 && address < 0x8000) { - ram_[address & 1023] = *cycle.value; - } else if(is_megacart_ && address >= 0xffc0) { - page_megacart(address); - } - break; + case CPU::Z80::PartialMachineCycle::Input: + switch((address >> 5) & 7) { + case 5: + update_video(); + *cycle.value = vdp_->get_register(address); + z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line()); + time_until_interrupt_ = vdp_->get_time_until_interrupt(); + break; - case CPU::Z80::PartialMachineCycle::Input: - switch((address >> 5) & 7) { - case 5: - update_video(); - *cycle.value = vdp_->get_register(address); - z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line()); - time_until_interrupt_ = vdp_->get_time_until_interrupt(); - break; + case 7: { + const std::size_t joystick_id = (address&2) >> 1; + Joystick *joystick = static_cast(joysticks_[joystick_id].get()); + if(joysticks_in_keypad_mode_) { + *cycle.value = joystick->get_keypad_input(); + } else { + *cycle.value = joystick->get_direction_input(); + } - case 7: { - const std::size_t joystick_id = (address&2) >> 1; - Joystick *joystick = static_cast(joysticks_[joystick_id].get()); - if(joysticks_in_keypad_mode_) { - *cycle.value = joystick->get_keypad_input(); - } else { - *cycle.value = joystick->get_direction_input(); - } + // Hitting exactly the recommended joypad input port is an indicator that + // this really is a ColecoVision game. The BIOS won't do this when just waiting + // to start a game (unlike accessing the VDP and SN). + if((address&0xfc) == 0xfc) confidence_counter_.add_hit(); + } break; - // Hitting exactly the recommended joypad input port is an indicator that - // this really is a ColecoVision game. The BIOS won't do this when just waiting - // to start a game (unlike accessing the VDP and SN). - if((address&0xfc) == 0xfc) confidence_counter_.add_hit(); - } break; + default: + switch(address&0xff) { + default: *cycle.value = 0xff; break; + case 0x52: + // Read AY data. + update_audio(); + ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1)); + *cycle.value = ay_.get_data_output(); + ay_.set_control_lines(GI::AY38910::ControlLines(0)); + break; + } + break; + } + break; - default: - switch(address&0xff) { - default: *cycle.value = 0xff; break; - case 0x52: - // Read AY data. - update_audio(); - ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1)); - *cycle.value = ay_.get_data_output(); - ay_.set_control_lines(GI::AY38910::ControlLines(0)); - break; - } - break; - } - break; + case CPU::Z80::PartialMachineCycle::Output: { + const int eighth = (address >> 5) & 7; + switch(eighth) { + case 4: case 6: + joysticks_in_keypad_mode_ = eighth == 4; + break; - case CPU::Z80::PartialMachineCycle::Output: { - const int eighth = (address >> 5) & 7; - switch(eighth) { - case 4: case 6: - joysticks_in_keypad_mode_ = eighth == 4; - break; + case 5: + update_video(); + vdp_->set_register(address, *cycle.value); + z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line()); + time_until_interrupt_ = vdp_->get_time_until_interrupt(); + break; - case 5: - update_video(); - vdp_->set_register(address, *cycle.value); - z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line()); - time_until_interrupt_ = vdp_->get_time_until_interrupt(); - break; + case 7: + update_audio(); + sn76489_.set_register(*cycle.value); + break; - case 7: - update_audio(); - sn76489_.set_register(*cycle.value); - break; + default: + // Catch Super Game Module accesses; it decodes more thoroughly. + switch(address&0xff) { + default: break; + case 0x7f: + super_game_module_.replace_bios = !((*cycle.value)&0x2); + break; + case 0x50: + // Set AY address. + update_audio(); + ay_.set_control_lines(GI::AY38910::BC1); + ay_.set_data_input(*cycle.value); + ay_.set_control_lines(GI::AY38910::ControlLines(0)); + break; + case 0x51: + // Set AY data. + update_audio(); + ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR)); + ay_.set_data_input(*cycle.value); + ay_.set_control_lines(GI::AY38910::ControlLines(0)); + break; + case 0x53: + super_game_module_.replace_ram = !!((*cycle.value)&0x1); + break; + } + break; + } + } break; - default: - // Catch Super Game Module accesses; it decodes more thoroughly. - switch(address&0xff) { - default: break; - case 0x7f: - super_game_module_.replace_bios = !((*cycle.value)&0x2); - break; - case 0x50: - // Set AY address. - update_audio(); - ay_.set_control_lines(GI::AY38910::BC1); - ay_.set_data_input(*cycle.value); - ay_.set_control_lines(GI::AY38910::ControlLines(0)); - break; - case 0x51: - // Set AY data. - update_audio(); - ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR)); - ay_.set_data_input(*cycle.value); - ay_.set_control_lines(GI::AY38910::ControlLines(0)); - break; - case 0x53: - super_game_module_.replace_ram = !!((*cycle.value)&0x1); - break; - } - break; - } - } break; - - default: break; + default: break; + } } if(time_until_interrupt_ > 0) { @@ -358,7 +370,7 @@ class ConcreteMachine: } CPU::Z80::Processor z80_; - std::unique_ptr vdp_; + std::unique_ptr vdp_; Concurrency::DeferringAsyncTaskQueue audio_queue_; TI::SN76489 sn76489_; diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp index 6c166a08b..24fe36c73 100644 --- a/Machines/MSX/MSX.cpp +++ b/Machines/MSX/MSX.cpp @@ -219,7 +219,7 @@ class ConcreteMachine: } void setup_output(float aspect_ratio) override { - vdp_.reset(new TI::TMS9918(TI::TMS9918::TMS9918A)); + vdp_.reset(new TI::TMS::TMS9918(TI::TMS::TMS9918A)); } void close_output() override { @@ -361,183 +361,185 @@ class ConcreteMachine: memory_slots_[2].cycles_since_update += total_length; memory_slots_[3].cycles_since_update += total_length; - uint16_t address = cycle.address ? *cycle.address : 0x0000; - switch(cycle.operation) { - case CPU::Z80::PartialMachineCycle::ReadOpcode: - if(use_fast_tape_) { - if(address == 0x1a63) { - // TAPION + if(cycle.is_terminal()) { + uint16_t address = cycle.address ? *cycle.address : 0x0000; + switch(cycle.operation) { + case CPU::Z80::PartialMachineCycle::ReadOpcode: + if(use_fast_tape_) { + if(address == 0x1a63) { + // TAPION - // Enable the tape motor. - i8255_.set_register(0xab, 0x8); + // Enable the tape motor. + i8255_.set_register(0xab, 0x8); - // Disable interrupts. - z80_.set_value_of_register(CPU::Z80::Register::IFF1, 0); - z80_.set_value_of_register(CPU::Z80::Register::IFF2, 0); + // Disable interrupts. + z80_.set_value_of_register(CPU::Z80::Register::IFF1, 0); + z80_.set_value_of_register(CPU::Z80::Register::IFF2, 0); - // Use the parser to find a header, and if one is found then populate - // LOWLIM and WINWID, and reset carry. Otherwise set carry. - using Parser = Storage::Tape::MSX::Parser; - std::unique_ptr new_speed = Parser::find_header(tape_player_); - if(new_speed) { - ram_[0xfca4] = new_speed->minimum_start_bit_duration; - ram_[0xfca5] = new_speed->low_high_disrimination_duration; - z80_.set_value_of_register(CPU::Z80::Register::Flags, 0); - } else { - z80_.set_value_of_register(CPU::Z80::Register::Flags, 1); + // Use the parser to find a header, and if one is found then populate + // LOWLIM and WINWID, and reset carry. Otherwise set carry. + using Parser = Storage::Tape::MSX::Parser; + std::unique_ptr new_speed = Parser::find_header(tape_player_); + if(new_speed) { + ram_[0xfca4] = new_speed->minimum_start_bit_duration; + ram_[0xfca5] = new_speed->low_high_disrimination_duration; + z80_.set_value_of_register(CPU::Z80::Register::Flags, 0); + } else { + z80_.set_value_of_register(CPU::Z80::Register::Flags, 1); + } + + // RET. + *cycle.value = 0xc9; + break; } - // RET. - *cycle.value = 0xc9; - break; - } + if(address == 0x1abc) { + // TAPIN - if(address == 0x1abc) { - // TAPIN + // Grab the current values of LOWLIM and WINWID. + using Parser = Storage::Tape::MSX::Parser; + Parser::FileSpeed tape_speed; + tape_speed.minimum_start_bit_duration = ram_[0xfca4]; + tape_speed.low_high_disrimination_duration = ram_[0xfca5]; - // Grab the current values of LOWLIM and WINWID. - using Parser = Storage::Tape::MSX::Parser; - Parser::FileSpeed tape_speed; - tape_speed.minimum_start_bit_duration = ram_[0xfca4]; - tape_speed.low_high_disrimination_duration = ram_[0xfca5]; + // Ask the tape parser to grab a byte. + int next_byte = Parser::get_byte(tape_speed, tape_player_); - // Ask the tape parser to grab a byte. - int next_byte = Parser::get_byte(tape_speed, tape_player_); + // If a byte was found, return it with carry unset. Otherwise set carry to + // indicate error. + if(next_byte >= 0) { + z80_.set_value_of_register(CPU::Z80::Register::A, static_cast(next_byte)); + z80_.set_value_of_register(CPU::Z80::Register::Flags, 0); + } else { + z80_.set_value_of_register(CPU::Z80::Register::Flags, 1); + } - // If a byte was found, return it with carry unset. Otherwise set carry to - // indicate error. - if(next_byte >= 0) { - z80_.set_value_of_register(CPU::Z80::Register::A, static_cast(next_byte)); - z80_.set_value_of_register(CPU::Z80::Register::Flags, 0); - } else { - z80_.set_value_of_register(CPU::Z80::Register::Flags, 1); + // RET. + *cycle.value = 0xc9; + break; } - - // RET. - *cycle.value = 0xc9; - break; } - } - if(!address) { - pc_zero_accesses_++; - } - if(read_pointers_[address >> 13] == unpopulated_) { - performed_unmapped_access_ = true; - } - pc_address_ = address; // This is retained so as to be able to name the source of an access to cartridge handlers. - case CPU::Z80::PartialMachineCycle::Read: - if(read_pointers_[address >> 13]) { - *cycle.value = read_pointers_[address >> 13][address & 8191]; - } else { + if(!address) { + pc_zero_accesses_++; + } + if(read_pointers_[address >> 13] == unpopulated_) { + performed_unmapped_access_ = true; + } + pc_address_ = address; // This is retained so as to be able to name the source of an access to cartridge handlers. + case CPU::Z80::PartialMachineCycle::Read: + if(read_pointers_[address >> 13]) { + *cycle.value = read_pointers_[address >> 13][address & 8191]; + } else { + int slot_hit = (paged_memory_ >> ((address >> 14) * 2)) & 3; + memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush()); + *cycle.value = memory_slots_[slot_hit].handler->read(address); + } + break; + + case CPU::Z80::PartialMachineCycle::Write: { + write_pointers_[address >> 13][address & 8191] = *cycle.value; + int slot_hit = (paged_memory_ >> ((address >> 14) * 2)) & 3; - memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush()); - *cycle.value = memory_slots_[slot_hit].handler->read(address); - } - break; - - case CPU::Z80::PartialMachineCycle::Write: { - write_pointers_[address >> 13][address & 8191] = *cycle.value; - - int slot_hit = (paged_memory_ >> ((address >> 14) * 2)) & 3; - if(memory_slots_[slot_hit].handler) { - update_audio(); - memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush()); - memory_slots_[slot_hit].handler->write(address, *cycle.value, read_pointers_[pc_address_ >> 13] != memory_slots_[0].read_pointers[pc_address_ >> 13]); - } - } break; - - case CPU::Z80::PartialMachineCycle::Input: - switch(address & 0xff) { - case 0x98: case 0x99: - vdp_->run_for(time_since_vdp_update_.flush()); - *cycle.value = vdp_->get_register(address); - z80_.set_interrupt_line(vdp_->get_interrupt_line()); - time_until_interrupt_ = vdp_->get_time_until_interrupt(); - break; - - case 0xa2: + if(memory_slots_[slot_hit].handler) { update_audio(); - ay_.set_control_lines(static_cast(GI::AY38910::BC2 | GI::AY38910::BC1)); - *cycle.value = ay_.get_data_output(); - ay_.set_control_lines(static_cast(0)); - break; - - case 0xa8: case 0xa9: - case 0xaa: case 0xab: - *cycle.value = i8255_.get_register(address); - break; - - default: - *cycle.value = 0xff; - break; - } - break; - - case CPU::Z80::PartialMachineCycle::Output: { - const int port = address & 0xff; - switch(port) { - case 0x98: case 0x99: - vdp_->run_for(time_since_vdp_update_.flush()); - vdp_->set_register(address, *cycle.value); - z80_.set_interrupt_line(vdp_->get_interrupt_line()); - time_until_interrupt_ = vdp_->get_time_until_interrupt(); - break; - - case 0xa0: case 0xa1: - update_audio(); - ay_.set_control_lines(static_cast(GI::AY38910::BDIR | GI::AY38910::BC2 | ((port == 0xa0) ? GI::AY38910::BC1 : 0))); - ay_.set_data_input(*cycle.value); - ay_.set_control_lines(static_cast(0)); - break; - - case 0xa8: case 0xa9: - case 0xaa: case 0xab: - i8255_.set_register(address, *cycle.value); - break; - - case 0xfc: case 0xfd: case 0xfe: case 0xff: -// printf("RAM banking %02x: %02x\n", port, *cycle.value); - break; - } - } break; - - case CPU::Z80::PartialMachineCycle::Interrupt: - *cycle.value = 0xff; - - // Take this as a convenient moment to jump into the keyboard buffer, if desired. - if(!input_text_.empty()) { - // The following are KEYBUF per the Red Book; its address and its definition as DEFS 40. - const int buffer_start = 0xfbf0; - const int buffer_size = 40; - - // Also from the Red Book: GETPNT is at F3FAH and PUTPNT is at F3F8H. - int read_address = ram_[0xf3fa] | (ram_[0xf3fb] << 8); - int write_address = ram_[0xf3f8] | (ram_[0xf3f9] << 8); - - // Write until either the string is exhausted or the write_pointer is immediately - // behind the read pointer; temporarily map write_address and read_address into - // buffer-relative values. - std::size_t characters_written = 0; - write_address -= buffer_start; - read_address -= buffer_start; - while(characters_written < input_text_.size()) { - const int next_write_address = (write_address + 1) % buffer_size; - if(next_write_address == read_address) break; - ram_[write_address + buffer_start] = static_cast(input_text_[characters_written]); - ++characters_written; - write_address = next_write_address; + memory_slots_[slot_hit].handler->run_for(memory_slots_[slot_hit].cycles_since_update.flush()); + memory_slots_[slot_hit].handler->write(address, *cycle.value, read_pointers_[pc_address_ >> 13] != memory_slots_[0].read_pointers[pc_address_ >> 13]); } - input_text_.erase(input_text_.begin(), input_text_.begin() + static_cast(characters_written)); + } break; - // Map the write address back into absolute terms and write it out again as PUTPNT. - write_address += buffer_start; - ram_[0xf3f8] = static_cast(write_address); - ram_[0xf3f9] = static_cast(write_address >> 8); - } - break; + case CPU::Z80::PartialMachineCycle::Input: + switch(address & 0xff) { + case 0x98: case 0x99: + vdp_->run_for(time_since_vdp_update_.flush()); + *cycle.value = vdp_->get_register(address); + z80_.set_interrupt_line(vdp_->get_interrupt_line()); + time_until_interrupt_ = vdp_->get_time_until_interrupt(); + break; - default: break; + case 0xa2: + update_audio(); + ay_.set_control_lines(static_cast(GI::AY38910::BC2 | GI::AY38910::BC1)); + *cycle.value = ay_.get_data_output(); + ay_.set_control_lines(static_cast(0)); + break; + + case 0xa8: case 0xa9: + case 0xaa: case 0xab: + *cycle.value = i8255_.get_register(address); + break; + + default: + *cycle.value = 0xff; + break; + } + break; + + case CPU::Z80::PartialMachineCycle::Output: { + const int port = address & 0xff; + switch(port) { + case 0x98: case 0x99: + vdp_->run_for(time_since_vdp_update_.flush()); + vdp_->set_register(address, *cycle.value); + z80_.set_interrupt_line(vdp_->get_interrupt_line()); + time_until_interrupt_ = vdp_->get_time_until_interrupt(); + break; + + case 0xa0: case 0xa1: + update_audio(); + ay_.set_control_lines(static_cast(GI::AY38910::BDIR | GI::AY38910::BC2 | ((port == 0xa0) ? GI::AY38910::BC1 : 0))); + ay_.set_data_input(*cycle.value); + ay_.set_control_lines(static_cast(0)); + break; + + case 0xa8: case 0xa9: + case 0xaa: case 0xab: + i8255_.set_register(address, *cycle.value); + break; + + case 0xfc: case 0xfd: case 0xfe: case 0xff: + // printf("RAM banking %02x: %02x\n", port, *cycle.value); + break; + } + } break; + + case CPU::Z80::PartialMachineCycle::Interrupt: + *cycle.value = 0xff; + + // Take this as a convenient moment to jump into the keyboard buffer, if desired. + if(!input_text_.empty()) { + // The following are KEYBUF per the Red Book; its address and its definition as DEFS 40. + const int buffer_start = 0xfbf0; + const int buffer_size = 40; + + // Also from the Red Book: GETPNT is at F3FAH and PUTPNT is at F3F8H. + int read_address = ram_[0xf3fa] | (ram_[0xf3fb] << 8); + int write_address = ram_[0xf3f8] | (ram_[0xf3f9] << 8); + + // Write until either the string is exhausted or the write_pointer is immediately + // behind the read pointer; temporarily map write_address and read_address into + // buffer-relative values. + std::size_t characters_written = 0; + write_address -= buffer_start; + read_address -= buffer_start; + while(characters_written < input_text_.size()) { + const int next_write_address = (write_address + 1) % buffer_size; + if(next_write_address == read_address) break; + ram_[write_address + buffer_start] = static_cast(input_text_[characters_written]); + ++characters_written; + write_address = next_write_address; + } + input_text_.erase(input_text_.begin(), input_text_.begin() + static_cast(characters_written)); + + // Map the write address back into absolute terms and write it out again as PUTPNT. + write_address += buffer_start; + ram_[0xf3f8] = static_cast(write_address); + ram_[0xf3f9] = static_cast(write_address >> 8); + } + break; + + default: break; + } } if(!tape_player_is_sleeping_) @@ -683,7 +685,7 @@ class ConcreteMachine: }; CPU::Z80::Processor z80_; - std::unique_ptr vdp_; + std::unique_ptr vdp_; Intel::i8255::i8255 i8255_; Concurrency::DeferringAsyncTaskQueue audio_queue_; diff --git a/Machines/MasterSystem/MasterSystem.cpp b/Machines/MasterSystem/MasterSystem.cpp new file mode 100644 index 000000000..e2cb42463 --- /dev/null +++ b/Machines/MasterSystem/MasterSystem.cpp @@ -0,0 +1,383 @@ +// +// MasterSystem.cpp +// Clock Signal +// +// Created by Thomas Harte on 20/09/2018. +// Copyright © 2018 Thomas Harte. All rights reserved. +// + +#include "MasterSystem.hpp" + +#include "../../Processors/Z80/Z80.hpp" + +#include "../../Components/9918/9918.hpp" +#include "../../Components/SN76489/SN76489.hpp" + +#include "../CRTMachine.hpp" +#include "../JoystickMachine.hpp" + +#include "../../ClockReceiver/ForceInline.hpp" + +#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" +#include "../../Outputs/Log.hpp" + +#include "../../Analyser/Static/Sega/Target.hpp" + +#include + +namespace { +const int sn76489_divider = 2; +} + +namespace Sega { +namespace MasterSystem { + +class Joystick: public Inputs::ConcreteJoystick { + public: + Joystick() : + ConcreteJoystick({ + Input(Input::Up), + Input(Input::Down), + Input(Input::Left), + Input(Input::Right), + + Input(Input::Fire, 0), + Input(Input::Fire, 1) + }) {} + + void did_set_input(const Input &digital_input, bool is_active) override { + switch(digital_input.type) { + default: return; + + case Input::Up: if(is_active) state_ &= ~0x01; else state_ |= 0x01; break; + case Input::Down: if(is_active) state_ &= ~0x02; else state_ |= 0x02; break; + case Input::Left: if(is_active) state_ &= ~0x04; else state_ |= 0x04; break; + case Input::Right: if(is_active) state_ &= ~0x08; else state_ |= 0x08; break; + case Input::Fire: + switch(digital_input.info.control.index) { + default: break; + case 0: if(is_active) state_ &= ~0x10; else state_ |= 0x10; break; + case 1: if(is_active) state_ &= ~0x20; else state_ |= 0x20; break; + } + break; + } + } + + uint8_t get_state() { + return state_; + } + + private: + uint8_t state_ = 0xff; +}; + +class ConcreteMachine: + public Machine, + public CPU::Z80::BusHandler, + public CRTMachine::Machine, + public JoystickMachine::Machine { + + public: + ConcreteMachine(const Analyser::Static::Sega::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : + model_(target.model), + z80_(*this), + sn76489_( + (target.model == Analyser::Static::Sega::Target::Model::SG1000) ? TI::SN76489::Personality::SN76489 : TI::SN76489::Personality::SMS, + audio_queue_, + sn76489_divider), + speaker_(sn76489_) { + speaker_.set_input_rate(3579545.0f / static_cast(sn76489_divider)); + set_clock_rate(3579545); + + // Instantiate the joysticks. + joysticks_.emplace_back(new Joystick); + joysticks_.emplace_back(new Joystick); + + // Clear the memory map. + map(read_pointers_, nullptr, 0x10000, 0); + map(write_pointers_, nullptr, 0x10000, 0); + + // Take a copy of the cartridge and place it into memory. + cartridge_ = target.media.cartridges[0]->get_segments()[0].data; + if(cartridge_.size() < 48*1024) { + std::size_t new_space = 48*1024 - cartridge_.size(); + cartridge_.resize(48*1024); + memset(&cartridge_[48*1024 - new_space], 0xff, new_space); + } + page_cartridge(); + + // Establish the BIOS (if relevant) and RAM. + if(target.model == Analyser::Static::Sega::Target::Model::MasterSystem) { + const auto roms = rom_fetcher("MasterSystem", {"bios.sms"}); + if(!roms[0]) { + throw ROMMachine::Error::MissingROMs; + } + + roms[0]->resize(8*1024); + memcpy(&bios_, roms[0]->data(), roms[0]->size()); + map(read_pointers_, bios_, 8*1024, 0); + + map(read_pointers_, ram_, 8*1024, 0xc000, 0x10000); + map(write_pointers_, ram_, 8*1024, 0xc000, 0x10000); + } else { + map(read_pointers_, ram_, 1024, 0xc000, 0x10000); + map(write_pointers_, ram_, 1024, 0xc000, 0x10000); + } + + speaker_.set_high_frequency_cutoff(8000); + } + + ~ConcreteMachine() { + audio_queue_.flush(); + } + + void setup_output(float aspect_ratio) override { + vdp_.reset(new TI::TMS::TMS9918(model_ == Analyser::Static::Sega::Target::Model::SG1000 ? TI::TMS::TMS9918A : TI::TMS::SMSVDP)); + get_crt()->set_video_signal(Outputs::CRT::VideoSignal::Composite); + } + + void close_output() override { + vdp_.reset(); + } + + Outputs::CRT::CRT *get_crt() override { + return vdp_->get_crt(); + } + + Outputs::Speaker::Speaker *get_speaker() override { + return &speaker_; + } + + void run_for(const Cycles cycles) override { + z80_.run_for(cycles); + } + + forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) { + time_since_vdp_update_ += cycle.length; + time_since_sn76489_update_ += cycle.length; + + if(cycle.is_terminal()) { + uint16_t address = cycle.address ? *cycle.address : 0x0000; + switch(cycle.operation) { + case CPU::Z80::PartialMachineCycle::ReadOpcode: + case CPU::Z80::PartialMachineCycle::Read: + *cycle.value = read_pointers_[address >> 10] ? read_pointers_[address >> 10][address & 1023] : 0xff; + break; + + case CPU::Z80::PartialMachineCycle::Write: + if(address >= 0xfffd && cartridge_.size() > 48*1024) { + if(paging_registers_[address - 0xfffd] != *cycle.value) { + paging_registers_[address - 0xfffd] = *cycle.value; + page_cartridge(); + } + } + + if(write_pointers_[address >> 10]) write_pointers_[address >> 10][address & 1023] = *cycle.value; + else LOG("Ignored write to ROM"); + break; + + case CPU::Z80::PartialMachineCycle::Input: + switch(address & 0xc1) { + case 0x00: + LOG("TODO: [input] memory control"); + *cycle.value = 0xff; + break; + case 0x01: + LOG("TODO: [input] I/O port control"); + *cycle.value = 0xff; + break; + case 0x40: + update_video(); + *cycle.value = vdp_->get_current_line(); + break; + case 0x41: + *cycle.value = vdp_->get_latched_horizontal_counter(); + break; + case 0x80: case 0x81: + update_video(); + *cycle.value = vdp_->get_register(address); + z80_.set_interrupt_line(vdp_->get_interrupt_line()); + time_until_interrupt_ = vdp_->get_time_until_interrupt(); + break; + case 0xc0: { + Joystick *const joypad1 = static_cast(joysticks_[0].get()); + Joystick *const joypad2 = static_cast(joysticks_[1].get()); + *cycle.value = static_cast(joypad1->get_state() | (joypad2->get_state() << 6)); + } break; + case 0xc1: { + Joystick *const joypad2 = static_cast(joysticks_[1].get()); + + *cycle.value = + (joypad2->get_state() >> 2) | + 0x30 | + get_th_values(); + } break; + + default: + ERROR("[input] Clearly some sort of typo"); + break; + } + break; + + case CPU::Z80::PartialMachineCycle::Output: + switch(address & 0xc1) { + case 0x00: + if(model_ == Analyser::Static::Sega::Target::Model::MasterSystem) { + // TODO: Obey the RAM enable. + memory_control_ = *cycle.value; + page_cartridge(); + } + break; + case 0x01: { + // A programmer can force the TH lines to 0 here, + // causing a phoney lightgun latch, so check for any + // discontinuity in TH inputs. + const auto previous_ths = get_th_values(); + io_port_control_ = *cycle.value; + const auto new_ths = get_th_values(); + + // Latch if either TH has newly gone to 1. + if((new_ths^previous_ths)&new_ths) { + update_video(); + vdp_->latch_horizontal_counter(); + } + } break; + case 0x40: case 0x41: + update_audio(); + sn76489_.set_register(*cycle.value); + break; + case 0x80: case 0x81: + update_video(); + vdp_->set_register(address, *cycle.value); + z80_.set_interrupt_line(vdp_->get_interrupt_line()); + time_until_interrupt_ = vdp_->get_time_until_interrupt(); + break; + case 0xc0: + LOG("TODO: [output] I/O port A/N; " << *cycle.value); + break; + case 0xc1: + LOG("TODO: [output] I/O port B/misc"); + break; + + default: + ERROR("[output] Clearly some sort of typo"); + break; + } + break; + + case CPU::Z80::PartialMachineCycle::Interrupt: + *cycle.value = 0xff; + break; + + default: break; + } + } + + if(time_until_interrupt_ > 0) { + time_until_interrupt_ -= cycle.length; + if(time_until_interrupt_ <= HalfCycles(0)) { + z80_.set_interrupt_line(true, time_until_interrupt_); + } + } + + return HalfCycles(0); + } + + void flush() { + update_video(); + update_audio(); + audio_queue_.perform(); + } + + std::vector> &get_joysticks() override { + return joysticks_; + } + + private: + inline uint8_t get_th_values() { + // Quick not on TH inputs here: if either is setup as an output, then the + // currently output level is returned. Otherwise they're fixed at 1. + return + static_cast( + ((io_port_control_ & 0x02) << 5) | ((io_port_control_&0x20) << 1) | + ((io_port_control_ & 0x08) << 4) | (io_port_control_&0x80) + ); + + } + + inline void update_audio() { + speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(sn76489_divider))); + } + inline void update_video() { + vdp_->run_for(time_since_vdp_update_.flush()); + } + + Analyser::Static::Sega::Target::Model model_; + CPU::Z80::Processor z80_; + std::unique_ptr vdp_; + + Concurrency::DeferringAsyncTaskQueue audio_queue_; + TI::SN76489 sn76489_; + Outputs::Speaker::LowpassSpeaker speaker_; + + std::vector> joysticks_; + + HalfCycles time_since_vdp_update_; + HalfCycles time_since_sn76489_update_; + HalfCycles time_until_interrupt_; + + uint8_t ram_[8*1024]; + uint8_t bios_[8*1024]; + std::vector cartridge_; + + uint8_t io_port_control_ = 0x0f; + + // The memory map has a 1kb granularity; this is determined by the SG1000's 1kb of RAM. + const uint8_t *read_pointers_[64]; + uint8_t *write_pointers_[64]; + template void map(T **target, uint8_t *source, size_t size, size_t start_address, size_t end_address = 0) { + if(!end_address) end_address = start_address + size; + for(auto address = start_address; address < end_address; address += 1024) { + target[address >> 10] = source ? &source[(address - start_address) & (size - 1)] : nullptr; + } + } + + uint8_t paging_registers_[3] = {0, 1, 2}; + uint8_t memory_control_ = 0; + void page_cartridge() { + // Either install the cartridge or don't. + if(!(memory_control_ & 0x40)) { + for(size_t c = 0; c < 3; ++c) { + const size_t start_addr = (paging_registers_[c] * 0x4000) % cartridge_.size(); + map( + read_pointers_, + cartridge_.data() + start_addr, + std::min(static_cast(0x4000), cartridge_.size() - start_addr), + c * 0x4000); + } + + // The first 1kb doesn't page though. + map(read_pointers_, cartridge_.data(), 0x400, 0x0000); + } else { + map(read_pointers_, nullptr, 0xc000, 0x0000); + } + + // Throw the BIOS on top if this machine has one and it isn't disabled. + if(model_ == Analyser::Static::Sega::Target::Model::MasterSystem && !(memory_control_ & 0x08)) { + map(read_pointers_, bios_, 8*1024, 0); + } + } +}; + +} +} + +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); +} + +Machine::~Machine() {} diff --git a/Machines/MasterSystem/MasterSystem.hpp b/Machines/MasterSystem/MasterSystem.hpp new file mode 100644 index 000000000..e6cfae681 --- /dev/null +++ b/Machines/MasterSystem/MasterSystem.hpp @@ -0,0 +1,27 @@ +// +// MasterSystem.hpp +// Clock Signal +// +// Created by Thomas Harte on 20/09/2018. +// Copyright © 2018 Thomas Harte. All rights reserved. +// + +#ifndef MasterSystem_hpp +#define MasterSystem_hpp + +#include "../../Analyser/Static/StaticAnalyser.hpp" +#include "../ROMMachine.hpp" + +namespace Sega { +namespace MasterSystem { + +class Machine { + public: + virtual ~Machine(); + static Machine *MasterSystem(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); +}; + +} +} + +#endif /* MasterSystem_hpp */ diff --git a/Machines/Utility/MachineForTarget.cpp b/Machines/Utility/MachineForTarget.cpp index 4d61e701e..44f0c43db 100644 --- a/Machines/Utility/MachineForTarget.cpp +++ b/Machines/Utility/MachineForTarget.cpp @@ -14,6 +14,7 @@ #include "../ColecoVision/ColecoVision.hpp" #include "../Commodore/Vic-20/Vic20.hpp" #include "../Electron/Electron.hpp" +#include "../MasterSystem/MasterSystem.hpp" #include "../MSX/MSX.hpp" #include "../Oric/Oric.hpp" #include "../ZX8081/ZX8081.hpp" @@ -36,6 +37,7 @@ namespace { Bind(Atari2600) BindD(Coleco::Vision, ColecoVision) Bind(Electron) + BindD(Sega::MasterSystem, MasterSystem) Bind(MSX) Bind(Oric) BindD(Commodore::Vic20, Vic20) diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index b52f8a493..95080054f 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -242,6 +242,10 @@ 4B7A90ED20410A85008514A2 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A90EC20410A85008514A2 /* StaticAnalyser.cpp */; }; 4B7BC7F51F58F27800D1B1B4 /* 6502AllRAM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B6A4C911F58F09E00E3F787 /* 6502AllRAM.cpp */; }; 4B7BC7F61F58F7D200D1B1B4 /* 6502Base.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B6A4C951F58F09E00E3F787 /* 6502Base.cpp */; }; + 4B7F188E2154825E00388727 /* MasterSystem.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7F188C2154825D00388727 /* MasterSystem.cpp */; }; + 4B7F188F2154825E00388727 /* MasterSystem.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7F188C2154825D00388727 /* MasterSystem.cpp */; }; + 4B7F1897215486A200388727 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7F1896215486A100388727 /* StaticAnalyser.cpp */; }; + 4B7F1898215486A200388727 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7F1896215486A100388727 /* StaticAnalyser.cpp */; }; 4B80AD001F85CACA00176895 /* BestEffortUpdater.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B80ACFE1F85CAC900176895 /* BestEffortUpdater.cpp */; }; 4B8334821F5D9FF70097E338 /* PartialMachineCycle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8334811F5D9FF70097E338 /* PartialMachineCycle.cpp */; }; 4B8334841F5DA0360097E338 /* Z80Storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8334831F5DA0360097E338 /* Z80Storage.cpp */; }; @@ -311,6 +315,7 @@ 4B9BE401203A0C0600FFAE60 /* MultiSpeaker.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B9BE3FE203A0C0600FFAE60 /* MultiSpeaker.cpp */; }; 4BA0F68E1EEA0E8400E9489E /* ZX8081.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BA0F68C1EEA0E8400E9489E /* ZX8081.cpp */; }; 4BA61EB01D91515900B3C876 /* NSData+StdVector.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BA61EAF1D91515900B3C876 /* NSData+StdVector.mm */; }; + 4BA91E1D216D85BA00F79557 /* MasterSystemVDPTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BA91E1C216D85BA00F79557 /* MasterSystemVDPTests.mm */; }; 4BAD13441FF709C700FD114A /* MSX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E61051FF34737002A9DBD /* MSX.cpp */; }; 4BAE49582032881E004BE78E /* CSZX8081.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B14978E1EE4B4D200CE2596 /* CSZX8081.mm */; }; 4BAE495920328897004BE78E /* ZX8081OptionsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B95FA9C1F11893B0008E395 /* ZX8081OptionsPanel.swift */; }; @@ -941,6 +946,10 @@ 4B7A90E42041097C008514A2 /* ColecoVision.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ColecoVision.cpp; sourceTree = ""; }; 4B7A90EB20410A85008514A2 /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StaticAnalyser.hpp; sourceTree = ""; }; 4B7A90EC20410A85008514A2 /* StaticAnalyser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StaticAnalyser.cpp; sourceTree = ""; }; + 4B7F188C2154825D00388727 /* MasterSystem.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MasterSystem.cpp; sourceTree = ""; }; + 4B7F188D2154825D00388727 /* MasterSystem.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MasterSystem.hpp; sourceTree = ""; }; + 4B7F1895215486A100388727 /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StaticAnalyser.hpp; sourceTree = ""; }; + 4B7F1896215486A100388727 /* StaticAnalyser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StaticAnalyser.cpp; sourceTree = ""; }; 4B80ACFE1F85CAC900176895 /* BestEffortUpdater.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = BestEffortUpdater.cpp; path = ../../Concurrency/BestEffortUpdater.cpp; sourceTree = ""; }; 4B80ACFF1F85CACA00176895 /* BestEffortUpdater.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = BestEffortUpdater.hpp; path = ../../Concurrency/BestEffortUpdater.hpp; sourceTree = ""; }; 4B8334811F5D9FF70097E338 /* PartialMachineCycle.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PartialMachineCycle.cpp; sourceTree = ""; }; @@ -1033,7 +1042,9 @@ 4BA141C12073100800A31EC9 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = ""; }; 4BA61EAE1D91515900B3C876 /* NSData+StdVector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+StdVector.h"; sourceTree = ""; }; 4BA61EAF1D91515900B3C876 /* NSData+StdVector.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSData+StdVector.mm"; sourceTree = ""; }; + 4BA91E1C216D85BA00F79557 /* MasterSystemVDPTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MasterSystemVDPTests.mm; sourceTree = ""; }; 4BA9C3CF1D8164A9002DDB61 /* MediaTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MediaTarget.hpp; sourceTree = ""; }; + 4BAA167B21582B1D008A3276 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = ""; }; 4BAB62AC1D3272D200DF5BA0 /* Disk.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Disk.hpp; sourceTree = ""; }; 4BAB62AE1D32730D00DF5BA0 /* Storage.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Storage.hpp; sourceTree = ""; }; 4BAF2B4C2004580C00480230 /* DMK.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DMK.cpp; sourceTree = ""; }; @@ -2191,6 +2202,25 @@ path = Coleco; sourceTree = ""; }; + 4B7F188B2154825D00388727 /* MasterSystem */ = { + isa = PBXGroup; + children = ( + 4B7F188C2154825D00388727 /* MasterSystem.cpp */, + 4B7F188D2154825D00388727 /* MasterSystem.hpp */, + ); + path = MasterSystem; + sourceTree = ""; + }; + 4B7F1894215486A100388727 /* Sega */ = { + isa = PBXGroup; + children = ( + 4B7F1895215486A100388727 /* StaticAnalyser.hpp */, + 4B7F1896215486A100388727 /* StaticAnalyser.cpp */, + 4BAA167B21582B1D008A3276 /* Target.hpp */, + ); + path = Sega; + sourceTree = ""; + }; 4B8334881F5DB8470097E338 /* Implementation */ = { isa = PBXGroup; children = ( @@ -2292,6 +2322,7 @@ 4BD67DC8209BE4D600AB2146 /* DiskII */, 4B89450F201967B4007DE474 /* MSX */, 4B8944F6201967B4007DE474 /* Oric */, + 4B7F1894215486A100388727 /* Sega */, 4B894504201967B4007DE474 /* ZX8081 */, ); path = Static; @@ -2789,10 +2820,11 @@ 4BB73EB51B587A5100552FC2 /* Clock SignalTests */ = { isa = PBXGroup; children = ( - 4B98A0601FFADCDE00ADF63B /* MSXStaticAnalyserTests.mm */, 4B5073091DDFCFDF00C48FBD /* ArrayBuilderTests.mm */, 4B924E981E74D22700B76AF1 /* AtariStaticAnalyserTests.mm */, 4BB2A9AE1E13367E001A5C23 /* CRCTests.mm */, + 4BA91E1C216D85BA00F79557 /* MasterSystemVDPTests.mm */, + 4B98A0601FFADCDE00ADF63B /* MSXStaticAnalyserTests.mm */, 4B121F9A1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm */, 4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */, 4B2AF8681E513FC20027EE29 /* TIATests.mm */, @@ -2832,11 +2864,11 @@ isa = PBXGroup; children = ( 4B54C0BB1F8D8E790050900F /* KeyboardMachine.cpp */, - 4BA9C3CF1D8164A9002DDB61 /* MediaTarget.hpp */, 4B046DC31CFE651500E9E45E /* CRTMachine.hpp */, 4BBB709C2020109C002FE009 /* DynamicMachine.hpp */, 4B7041271F92C26900735E45 /* JoystickMachine.hpp */, 4B8E4ECD1DCE483D003716C3 /* KeyboardMachine.hpp */, + 4BA9C3CF1D8164A9002DDB61 /* MediaTarget.hpp */, 4BDCC5F81FB27A5E001220C5 /* ROMMachine.hpp */, 4B38F3491F2EC12000D9235D /* AmstradCPC */, 4B15AA082082C799005E6C8D /* AppleII */, @@ -2844,6 +2876,7 @@ 4B7A90E22041097C008514A2 /* ColecoVision */, 4B4DC81D1D2C2425003C5BF8 /* Commodore */, 4B2E2D9E1C3A070900138695 /* Electron */, + 4B7F188B2154825D00388727 /* MasterSystem */, 4B79A4FC1FC8FF9800EEDAD5 /* MSX */, 4BCF1FA51DADC3E10039D2E7 /* Oric */, 4B2B3A461F9B8FA70062DABF /* Utility */, @@ -3628,6 +3661,7 @@ 4B055AD51FAE9B0B0060FFFF /* Video.cpp in Sources */, 4B055AE11FAE9B6F0060FFFF /* ArrayBuilder.cpp in Sources */, 4B894521201967B4007DE474 /* StaticAnalyser.cpp in Sources */, + 4B7F188F2154825E00388727 /* MasterSystem.cpp in Sources */, 4B055AA51FAE85EF0060FFFF /* Encoder.cpp in Sources */, 4B894529201967B4007DE474 /* Disk.cpp in Sources */, 4B055AEA1FAE9B990060FFFF /* 6502Storage.cpp in Sources */, @@ -3641,6 +3675,7 @@ 4B894531201967B4007DE474 /* StaticAnalyser.cpp in Sources */, 4BC891AE20F6EAB300EDE5B3 /* Rectangle.cpp in Sources */, 4B894539201967B4007DE474 /* Tape.cpp in Sources */, + 4B7F1898215486A200388727 /* StaticAnalyser.cpp in Sources */, 4B055AE51FAE9B6F0060FFFF /* IntermediateShader.cpp in Sources */, 4B15A9FD208249BB005E6C8D /* StaticAnalyser.cpp in Sources */, 4B055AD31FAE9B0B0060FFFF /* Microdisc.cpp in Sources */, @@ -3864,6 +3899,7 @@ 4B12C0ED1FCFA98D005BFD93 /* Keyboard.cpp in Sources */, 4BA0F68E1EEA0E8400E9489E /* ZX8081.cpp in Sources */, 4BD468F71D8DF41D0084958B /* 1770.cpp in Sources */, + 4B7F1897215486A200388727 /* StaticAnalyser.cpp in Sources */, 4BD3A30B1EE755C800B5B501 /* Video.cpp in Sources */, 4BBF99141C8FBA6F0075DAFB /* TextureBuilder.cpp in Sources */, 4B5FADBA1DE3151600AEC565 /* FileHolder.cpp in Sources */, @@ -3895,6 +3931,7 @@ 4B6A4C991F58F09E00E3F787 /* 6502Base.cpp in Sources */, 4B4518871F75E91A00926311 /* DigitalPhaseLockedLoop.cpp in Sources */, 4B98A05E1FFAD3F600ADF63B /* CSROMFetcher.mm in Sources */, + 4B7F188E2154825E00388727 /* MasterSystem.cpp in Sources */, 4B8805F41DCFD22A003085B1 /* Commodore.cpp in Sources */, 4B3FCC40201EC24200960631 /* MultiMachine.cpp in Sources */, 4B2E2D9A1C3A06EC00138695 /* Atari2600.cpp in Sources */, @@ -3966,6 +4003,7 @@ 4BB73EB71B587A5100552FC2 /* AllSuiteATests.swift in Sources */, 4B01A6881F22F0DB001FD6E3 /* Z80MemptrTests.swift in Sources */, 4B121F9B1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm in Sources */, + 4BA91E1D216D85BA00F79557 /* MasterSystemVDPTests.mm in Sources */, 4B98A0611FFADCDE00ADF63B /* MSXStaticAnalyserTests.mm in Sources */, 4BEF6AAC1D35D1C400E73575 /* DPLLTests.swift in Sources */, 4B3BA0CF1D318B44005DD7A7 /* MOS6522Bridge.mm in Sources */, diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist index 0e4491399..1fb197ed1 100644 --- a/OSBindings/Mac/Clock Signal/Info.plist +++ b/OSBindings/Mac/Clock Signal/Info.plist @@ -431,6 +431,50 @@ LSHandlerRank Owner + + CFBundleTypeExtensions + + sms + + CFBundleTypeOSTypes + + ???? + + CFBundleTypeIconFile + cartridge.png + CFBundleTypeName + Master System Cartridge + CFBundleTypeRole + Viewer + LSTypeIsPackage + + NSDocumentClass + $(PRODUCT_MODULE_NAME).MachineDocument + LSHandlerRank + Owner + + + CFBundleTypeExtensions + + sg + + CFBundleTypeOSTypes + + ???? + + CFBundleTypeIconFile + cartridge.png + CFBundleTypeName + SG1000 Cartridge + CFBundleTypeRole + Viewer + LSTypeIsPackage + + NSDocumentClass + $(PRODUCT_MODULE_NAME).MachineDocument + LSHandlerRank + Owner + CFBundleTypeExtensions diff --git a/OSBindings/Mac/Clock SignalTests/MasterSystemVDPTests.mm b/OSBindings/Mac/Clock SignalTests/MasterSystemVDPTests.mm new file mode 100644 index 000000000..ec5c36587 --- /dev/null +++ b/OSBindings/Mac/Clock SignalTests/MasterSystemVDPTests.mm @@ -0,0 +1,82 @@ +// +// MasterSystemVDPTests.m +// Clock SignalTests +// +// Created by Thomas Harte on 09/10/2018. +// Copyright © 2018 Thomas Harte. All rights reserved. +// + +#import +#import + +#include "9918.hpp" + +@interface MasterSystemVDPTests : XCTestCase +@end + +@implementation MasterSystemVDPTests { + NSOpenGLContext *_openGLContext; +} + +- (void)setUp { + [super setUp]; + + // Create a valid OpenGL context, so that a VDP can be constructed. + NSOpenGLPixelFormatAttribute attributes[] = + { + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, + 0 + }; + + NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes]; + _openGLContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil]; + [_openGLContext makeCurrentContext]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + _openGLContext = nil; + + [super tearDown]; +} + +- (void)testLineInterrupt { + TI::TMS::TMS9918 vdp(TI::TMS::Personality::SMSVDP); + + // Disable end-of-frame interrupts, enable line interrupts. + vdp.set_register(1, 0x00); + vdp.set_register(1, 0x81); + + vdp.set_register(1, 0x10); + vdp.set_register(1, 0x80); + + // Set a line interrupt to occur in five lines. + vdp.set_register(1, 5); + vdp.set_register(1, 0x8a); + + // Get time until interrupt. + int time_until_interrupt = vdp.get_time_until_interrupt().as_int() - 1; + + // Check interrupt flag isn't set prior to the reported time. + vdp.run_for(HalfCycles(time_until_interrupt)); + NSAssert(!vdp.get_interrupt_line(), @"Interrupt line went active early [1]"); + + // Check interrupt flag is set at the reported time. + vdp.run_for(HalfCycles(1)); + NSAssert(vdp.get_interrupt_line(), @"Interrupt line wasn't set when promised [1]"); + + // Read the status register to clear interrupt status. + vdp.get_register(1); + NSAssert(!vdp.get_interrupt_line(), @"Interrupt wasn't reset by status read"); + + // Check interrupt flag isn't set prior to the reported time. + time_until_interrupt = vdp.get_time_until_interrupt().as_int() - 1; + vdp.run_for(HalfCycles(time_until_interrupt)); + NSAssert(!vdp.get_interrupt_line(), @"Interrupt line went active early [2]"); + + // Check interrupt flag is set at the reported time. + vdp.run_for(HalfCycles(1)); + NSAssert(vdp.get_interrupt_line(), @"Interrupt line wasn't set when promised [2]"); +} + +@end diff --git a/OSBindings/SDL/SConstruct b/OSBindings/SDL/SConstruct index 3b64bd8db..ebcbefbad 100644 --- a/OSBindings/SDL/SConstruct +++ b/OSBindings/SDL/SConstruct @@ -25,6 +25,7 @@ SOURCES += glob.glob('../../Analyser/Static/Disassembler/*.cpp') SOURCES += glob.glob('../../Analyser/Static/DiskII/*.cpp') SOURCES += glob.glob('../../Analyser/Static/MSX/*.cpp') SOURCES += glob.glob('../../Analyser/Static/Oric/*.cpp') +SOURCES += glob.glob('../../Analyser/Static/Sega/*.cpp') SOURCES += glob.glob('../../Analyser/Static/ZX8081/*.cpp') SOURCES += glob.glob('../../Components/1770/*.cpp') @@ -54,6 +55,7 @@ SOURCES += glob.glob('../../Machines/Commodore/*.cpp') SOURCES += glob.glob('../../Machines/Commodore/1540/Implementation/*.cpp') SOURCES += glob.glob('../../Machines/Commodore/Vic-20/*.cpp') SOURCES += glob.glob('../../Machines/Electron/*.cpp') +SOURCES += glob.glob('../../Machines/MasterSystem/*.cpp') SOURCES += glob.glob('../../Machines/MSX/*.cpp') SOURCES += glob.glob('../../Machines/Oric/*.cpp') SOURCES += glob.glob('../../Machines/Utility/*.cpp') diff --git a/ROMImages/MasterSystem/readme.txt b/ROMImages/MasterSystem/readme.txt new file mode 100644 index 000000000..ed5073cc9 --- /dev/null +++ b/ROMImages/MasterSystem/readme.txt @@ -0,0 +1,5 @@ +BIOS files would ordinarily go here; the copyright status of these is uncertain so they have not been included in this repository. + +Expected files: + +bios.sms — the EU/US Master System BIOS. \ No newline at end of file diff --git a/Storage/TargetPlatforms.hpp b/Storage/TargetPlatforms.hpp index e6718dbf4..37017e236 100644 --- a/Storage/TargetPlatforms.hpp +++ b/Storage/TargetPlatforms.hpp @@ -24,10 +24,11 @@ enum Type: IntType { ColecoVision = 1 << 9, Commodore = 1 << 10, DiskII = 1 << 11, - MSX = 1 << 12, - Oric = 1 << 13, - ZX80 = 1 << 14, - ZX81 = 1 << 15, + Sega = 1 << 12, + MSX = 1 << 13, + Oric = 1 << 14, + ZX80 = 1 << 15, + ZX81 = 1 << 16, Acorn = AcornAtom | AcornElectron | BBCMaster | BBCModelA | BBCModelB, ZX8081 = ZX80 | ZX81,