diff --git a/Analyser/Static/MSX/StaticAnalyser.cpp b/Analyser/Static/MSX/StaticAnalyser.cpp index b2bdf02de..322189ca4 100644 --- a/Analyser/Static/MSX/StaticAnalyser.cpp +++ b/Analyser/Static/MSX/StaticAnalyser.cpp @@ -37,6 +37,11 @@ static std::unique_ptr CartridgeTarget( auto target = std::make_unique(); target->confidence = confidence; + // Observation: all ROMs of 48kb or less are from the MSX 1 era. + if(segment.data.size() < 48*1024) { + target->model = Analyser::Static::MSX::Target::Model::MSX1; + } + if(type == Analyser::Static::MSX::Cartridge::Type::None) { target->media.cartridges.emplace_back(new Storage::Cartridge::Cartridge(output_segments)); } else { @@ -100,6 +105,7 @@ static Analyser::Static::TargetList CartridgeTargetsFrom( // TODO: check for a rational init address? // If this ROM is less than 48kb in size then it's an ordinary ROM. Just emplace it and move on. + // Bonus observation: all such ROMs are from the MSX 1 era. if(data_size <= 0xc000) { targets.emplace_back(CartridgeTarget(segment, start_address, Analyser::Static::MSX::Cartridge::Type::None, 1.0)); continue; diff --git a/Analyser/Static/MSX/Target.hpp b/Analyser/Static/MSX/Target.hpp index 06e590f41..13b95eff1 100644 --- a/Analyser/Static/MSX/Target.hpp +++ b/Analyser/Static/MSX/Target.hpp @@ -26,7 +26,7 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl< MSX1, MSX2 ); - Model model = Model::MSX1; + Model model = Model::MSX2; ReflectableEnum(Region, Japan, diff --git a/ClockReceiver/JustInTime.hpp b/ClockReceiver/JustInTime.hpp index 040b26e4d..939ff0684 100644 --- a/ClockReceiver/JustInTime.hpp +++ b/ClockReceiver/JustInTime.hpp @@ -14,6 +14,8 @@ #include "ClockingHintSource.hpp" #include "ForceInline.hpp" +#include + /*! A JustInTimeActor holds (i) an embedded object with a run_for method; and (ii) an amount of time since run_for was last called. @@ -121,7 +123,13 @@ template () { +#ifndef NDEBUG + assert(!flush_concurrency_check_.test_and_set()); +#endif flush(); +#ifndef NDEBUG + flush_concurrency_check_.clear(); +#endif return std::unique_ptr(&object_, SequencePointAwareDeleter(this)); } @@ -130,7 +138,13 @@ template () const { auto non_const_this = const_cast *>(this); +#ifndef NDEBUG + assert(!non_const_this->flush_concurrency_check_.test_and_set()); +#endif non_const_this->flush(); +#ifndef NDEBUG + non_const_this->flush_concurrency_check_.clear(); +#endif return std::unique_ptr(&object_, SequencePointAwareDeleter(non_const_this)); } @@ -264,6 +278,10 @@ template -namespace TI { -namespace TMS { +namespace TI::TMS { enum Personality { TMS9918A, // includes the 9928 and 9929; set TV standard and output device as desired. @@ -36,13 +35,11 @@ enum class TVStandard { NTSC }; -} } #include "Implementation/9918Base.hpp" -namespace TI { -namespace TMS { +namespace TI::TMS { /*! Provides emulation of the TMS9918a, TMS9928 and TMS9929. Likely in the future to be the @@ -127,7 +124,6 @@ template class TMS9918: private Base { bool get_interrupt_line() const; }; -} } #endif /* TMS9918_hpp */ diff --git a/Components/9918/Implementation/9918.cpp b/Components/9918/Implementation/9918.cpp index ff05d409e..61a227235 100644 --- a/Components/9918/Implementation/9918.cpp +++ b/Components/9918/Implementation/9918.cpp @@ -31,24 +31,44 @@ Base::Base() : // into whether there's a more natural form. It feels unlikely given the diversity of chips modelled. if constexpr (is_sega_vdp(personality)) { - mode_timing_.line_interrupt_position = 64; + // Cf. https://www.smspower.org/forums/8161-SMSDisplayTiming - mode_timing_.end_of_frame_interrupt_position.column = 63; + // "For a line interrupt, /INT is pulled low 608 mclks into the appropriate scanline relative to pixel 0. + // This is 3 mclks before the rising edge of /HSYNC which starts the next scanline." + mode_timing_.line_interrupt_position = (LineLayout::EndOfLeftBorder + 304) % Timing::CyclesPerLine; + + // For a frame interrupt, /INT is pulled low 607 mclks into scanline 192 (of scanlines 0 through 261) relative to pixel 0. + // This is 4 mclks before the rising edge of /HSYNC which starts the next scanline. + mode_timing_.end_of_frame_interrupt_position.column = mode_timing_.line_interrupt_position - 1; mode_timing_.end_of_frame_interrupt_position.row = 193; } - // Establish that output is delayed after reading by `output_lag` cycles; start - // at a random position. - read_pointer_.row = rand() % 262; - read_pointer_.column = rand() % (Timing::CyclesPerLine - output_lag); - write_pointer_.row = read_pointer_.row; - write_pointer_.column = read_pointer_.column + output_lag; + if constexpr (is_yamaha_vdp(personality)) { + // TODO: start of sync, or end of sync? + mode_timing_.line_interrupt_position = 0;//Timing::StartOfSync; + } + + // Establish that output is delayed after reading by `output_lag` cycles, + // i.e. the fetch pointer is currently _ahead_ of the output pointer. + output_pointer_.row = output_pointer_.column = 0; + + fetch_pointer_ = output_pointer_; + fetch_pointer_.column += output_lag; + + fetch_line_buffer_ = line_buffers_.begin(); + draw_line_buffer_ = line_buffers_.begin(); + fetch_sprite_buffer_ = sprite_buffers_.begin(); } template TMS9918::TMS9918() { this->crt_.set_display_type(Outputs::Display::DisplayType::RGB); - this->crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f)); + + if constexpr (is_yamaha_vdp(personality)) { + this->crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.065f, 0.875f, 0.875f)); + } else { + this->crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f)); + } // The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement // intended to produce the correct relationship between the hard edges between pixels and @@ -98,7 +118,7 @@ Outputs::Display::DisplayType TMS9918::get_display_type() const { return this->crt_.get_display_type(); } -void LineBuffer::reset_sprite_collection() { +void SpriteBuffer::reset_sprite_collection() { sprites_stopped = false; active_sprite_slot = 0; @@ -108,30 +128,43 @@ void LineBuffer::reset_sprite_collection() { } template -void Base::posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_position, int screen_row) { +void Base::posit_sprite(int sprite_number, int sprite_position, uint8_t screen_row) { + // Evaluation of visibility of sprite 0 is always the first step in + // populating a sprite buffer; so use it to uncork a new one. + if(!sprite_number) { + advance(fetch_sprite_buffer_); + fetched_sprites_ = &*fetch_sprite_buffer_; + fetch_sprite_buffer_->reset_sprite_collection(); + fetch_sprite_buffer_->sprite_terminator = mode_timing_.sprite_terminator(fetch_line_buffer_->screen_mode); + + if constexpr (SpriteBuffer::test_is_filling) { + fetch_sprite_buffer_->is_filling = true; + } + } + if(!(status_ & StatusSpriteOverflow)) { status_ = uint8_t((status_ & ~0x1f) | (sprite_number & 0x1f)); } - if(buffer.sprites_stopped) return; + if(fetch_sprite_buffer_->sprites_stopped) return; // A sprite Y of 208 means "don't scan the list any further". - if(mode_timing_.allow_sprite_terminator && sprite_position == mode_timing_.sprite_terminator) { - buffer.sprites_stopped = true; + if(mode_timing_.allow_sprite_terminator && sprite_position == fetch_sprite_buffer_->sprite_terminator) { + fetch_sprite_buffer_->sprites_stopped = true; return; } - const int sprite_row = (((screen_row + 1) % mode_timing_.total_lines) - ((sprite_position + 1) & 255)) & 255; + const auto sprite_row = uint8_t(screen_row - sprite_position); if(sprite_row < 0 || sprite_row >= sprite_height_) return; - if(buffer.active_sprite_slot == mode_timing_.maximum_visible_sprites) { + if(fetch_sprite_buffer_->active_sprite_slot == mode_timing_.maximum_visible_sprites) { status_ |= StatusSpriteOverflow; return; } - LineBuffer::ActiveSprite &sprite = buffer.active_sprites[buffer.active_sprite_slot]; + auto &sprite = fetch_sprite_buffer_->active_sprites[fetch_sprite_buffer_->active_sprite_slot]; sprite.index = sprite_number; sprite.row = sprite_row >> (sprites_magnified_ ? 1 : 0); - ++buffer.active_sprite_slot; + ++fetch_sprite_buffer_->active_sprite_slot; } template @@ -147,40 +180,44 @@ void TMS9918::run_for(const HalfCycles cycles) { const int int_cycles = this->clock_converter_.to_internal(cycles.as()); if(!int_cycles) return; - // There are two intertwined processes here, 'writing' (which means writing to the - // line buffers, i.e. it's everything to do with collecting a line) and 'reading' - // (which means reading from the line buffers and generating video). - int write_cycles_pool = int_cycles; - int read_cycles_pool = int_cycles; + // There are two intertwined processes here, 'fetching' (i.e. writing to the + // line buffers with newly-fetched video contents) and 'output' (reading from + // the line buffers and generating video). + int fetch_cycles_pool = int_cycles; + int output_cycles_pool = int_cycles; - while(write_cycles_pool || read_cycles_pool) { + while(fetch_cycles_pool || output_cycles_pool) { #ifndef NDEBUG - LineBufferPointer backup = this->read_pointer_; + LineBufferPointer backup = this->output_pointer_; #endif - if(write_cycles_pool) { - // Determine how much writing to do. - const int write_cycles = std::min( - Timing::CyclesPerLine - this->write_pointer_.column, - write_cycles_pool + if(fetch_cycles_pool) { + // Determine how much writing to do; at the absolute most go to the end of this line. + const int fetch_cycles = std::min( + Timing::CyclesPerLine - this->fetch_pointer_.column, + fetch_cycles_pool ); - const int end_column = this->write_pointer_.column + write_cycles; - LineBuffer &line_buffer = this->line_buffers_[this->write_pointer_.row]; + const int end_column = this->fetch_pointer_.column + fetch_cycles; - // Determine what this does to any enqueued VRAM access. - this->minimum_access_column_ = this->write_pointer_.column + this->cycles_until_access_; - this->cycles_until_access_ -= write_cycles; + // ... and to any pending Yamaha commands. + if constexpr (is_yamaha_vdp(personality)) { + if(Storage::command_) { + Storage::minimum_command_column_ = + this->fetch_pointer_.column + Storage::command_->cycles; + Storage::command_->cycles -= fetch_cycles; + } + } // --------------------------------------- // Latch scrolling position, if necessary. // --------------------------------------- if constexpr (is_sega_vdp(personality)) { - if(this->write_pointer_.column < 61 && end_column >= 61) { - if(!this->write_pointer_.row) { - this->master_system_.latched_vertical_scroll = this->master_system_.vertical_scroll; + if(this->fetch_pointer_.column < 61 && end_column >= 61) { + if(!this->fetch_pointer_.row) { + Storage::latched_vertical_scroll_ = Storage::vertical_scroll_; - if(this->master_system_.mode4_enable) { + if(Storage::mode4_enable_) { this->mode_timing_.pixel_lines = 192; if(this->mode2_enable_ && this->mode1_enable_) this->mode_timing_.pixel_lines = 224; if(this->mode2_enable_ && this->mode3_enable_) this->mode_timing_.pixel_lines = 240; @@ -191,7 +228,7 @@ void TMS9918::run_for(const HalfCycles cycles) { this->mode_timing_.end_of_frame_interrupt_position.row = this->mode_timing_.pixel_lines + 1; } } - line_buffer.latched_horizontal_scroll = this->master_system_.horizontal_scroll; + this->fetch_line_buffer_->latched_horizontal_scroll = Storage::horizontal_scroll_; } } @@ -200,53 +237,71 @@ void TMS9918::run_for(const HalfCycles cycles) { // ------------------------ // Perform memory accesses. // ------------------------ -#define fetch(function, clock) \ - const int first_window = from_internal(this->write_pointer_.column);\ - const int final_window = from_internal(end_column); \ - if(first_window == final_window) break; \ - if(final_window != clock_rate()) { \ - function(first_window, final_window); \ - } else { \ - function(first_window, final_window); \ - } +#define fetch(function, clock, offset) { \ + const int first_window = from_internal(this->fetch_pointer_.column); \ + const int final_window = from_internal(end_column); \ + if(first_window == final_window) break; \ + const auto y = uint8_t( \ + this->fetch_line_buffer_->vertical_state == VerticalState::Prefetch ? \ + offset - 1 : (this->fetch_pointer_.row + offset)); \ + if(final_window != clock_rate()) { \ + function(y, first_window, final_window); \ + } else { \ + function(y, first_window, final_window); \ + } \ +} - switch(line_buffer.line_mode) { - case LineMode::Text: { fetch(this->template fetch_tms_text, Clock::TMSMemoryWindow); } break; - case LineMode::Character: { fetch(this->template fetch_tms_character, Clock::TMSMemoryWindow); } break; - case LineMode::SMS: { fetch(this->template fetch_sms, Clock::TMSMemoryWindow); } break; - case LineMode::Refresh: { fetch(this->template fetch_tms_refresh, Clock::TMSMemoryWindow); } break; + + + if constexpr (is_yamaha_vdp(personality)) { + fetch(this->template fetch_yamaha, Clock::Internal, Storage::vertical_offset_); + } else { + switch(this->fetch_line_buffer_->fetch_mode) { + case FetchMode::Text: fetch(this->template fetch_tms_text, Clock::TMSMemoryWindow, 0); break; + case FetchMode::Character: fetch(this->template fetch_tms_character, Clock::TMSMemoryWindow, 0); break; + case FetchMode::SMS: fetch(this->template fetch_sms, Clock::TMSMemoryWindow, 0); break; + case FetchMode::Refresh: fetch(this->template fetch_tms_refresh, Clock::TMSMemoryWindow, 0); break; + + default: break; + } } #undef fetch - // ------------------------------- // Check for interrupt conditions. // ------------------------------- - if(this->write_pointer_.column < this->mode_timing_.line_interrupt_position && end_column >= this->mode_timing_.line_interrupt_position) { + if(this->fetch_pointer_.column < this->mode_timing_.line_interrupt_position && end_column >= this->mode_timing_.line_interrupt_position) { // The Sega VDP offers a decrementing counter for triggering line interrupts; // it is reloaded either when it overflows or upon every non-pixel line after the first. // It is otherwise decremented. if constexpr (is_sega_vdp(personality)) { - if(this->write_pointer_.row >= 0 && this->write_pointer_.row <= this->mode_timing_.pixel_lines) { - --this->line_interrupt_counter; - if(this->line_interrupt_counter == 0xff) { + if(this->fetch_pointer_.row >= 0 && this->fetch_pointer_.row <= this->mode_timing_.pixel_lines) { + if(!this->line_interrupt_counter_) { this->line_interrupt_pending_ = true; - this->line_interrupt_counter = this->line_interrupt_target; + this->line_interrupt_counter_ = this->line_interrupt_target_; + } else { + --this->line_interrupt_counter_; } } else { - this->line_interrupt_counter = this->line_interrupt_target; + this->line_interrupt_counter_ = this->line_interrupt_target_; } } - // TODO: the V9938 provides line interrupts from direct specification of the target line. - // So life is easy. + if constexpr (is_yamaha_vdp(personality)) { + if( + this->vertical_active_ && + this->fetch_pointer_.row == ((this->line_interrupt_target_ - Storage::vertical_offset_) & 0xff) + ) { + this->line_interrupt_pending_ = true; + } + } } if( - this->write_pointer_.row == this->mode_timing_.end_of_frame_interrupt_position.row && - this->write_pointer_.column < this->mode_timing_.end_of_frame_interrupt_position.column && + this->fetch_pointer_.row == this->mode_timing_.end_of_frame_interrupt_position.row && + this->fetch_pointer_.column < this->mode_timing_.end_of_frame_interrupt_position.column && end_column >= this->mode_timing_.end_of_frame_interrupt_position.column ) { this->status_ |= StatusInterrupt; @@ -257,86 +312,177 @@ void TMS9918::run_for(const HalfCycles cycles) { // ------------- // Advance time. // ------------- - this->write_pointer_.column = end_column; - write_cycles_pool -= write_cycles; + this->fetch_pointer_.column = end_column; + fetch_cycles_pool -= fetch_cycles; - if(this->write_pointer_.column == Timing::CyclesPerLine) { - this->write_pointer_.column = 0; - this->write_pointer_.row = (this->write_pointer_.row + 1) % this->mode_timing_.total_lines; - LineBuffer &next_line_buffer = this->line_buffers_[this->write_pointer_.row]; + // Check for end of line. + if(this->fetch_pointer_.column == Timing::CyclesPerLine) { + this->fetch_pointer_.column = 0; + this->fetch_pointer_.row = (this->fetch_pointer_.row + 1) % this->mode_timing_.total_lines; + + this->vertical_active_ |= !this->fetch_pointer_.row; + this->vertical_active_ &= this->fetch_pointer_.row != this->mode_timing_.pixel_lines; + + // Yamaha: handle blinking. + if constexpr (is_yamaha_vdp(personality)) { + if(!this->fetch_pointer_.row && Storage::blink_periods_) { + --Storage::blink_counter_; + while(!Storage::blink_counter_) { + Storage::in_blink_ ^= 1; + Storage::blink_counter_ = (Storage::blink_periods_ >> (Storage::in_blink_ << 2)) & 0xf; + } + } + } + + // Progress towards any delayed events. + this->minimum_access_column_ = + std::max( + 0, + this->minimum_access_column_ - Timing::CyclesPerLine + ); + if constexpr (is_yamaha_vdp(personality)) { + Storage::minimum_command_column_ = + std::max( + 0, + Storage::minimum_command_column_ - Timing::CyclesPerLine + ); + } + + this->advance(this->fetch_line_buffer_); + if(this->fetched_sprites_ && this->fetched_sprites_->active_sprite_slot) { + this->fetch_line_buffer_->sprites = this->fetched_sprites_; + this->fetched_sprites_ = nullptr; + } else { + this->fetch_line_buffer_->sprites = nullptr; + } // Establish the current screen output mode, which will be captured as a // line mode momentarily. - this->screen_mode_ = this->current_screen_mode(); + this->screen_mode_ = this->template current_screen_mode(); + this->underlying_mode_ = this->template current_screen_mode(); + + if constexpr (is_yamaha_vdp(personality)) { + auto &desc = Storage::mode_description_; + desc.pixels_per_byte = pixels_per_byte(this->underlying_mode_); + desc.width = width(this->underlying_mode_); + desc.rotate_address = interleaves_banks(this->underlying_mode_); + } // Based on the output mode, pick a line mode. - next_line_buffer.first_pixel_output_column = Timing::FirstPixelCycle; - next_line_buffer.next_border_column = Timing::CyclesPerLine; - next_line_buffer.pixel_count = 256; + this->fetch_line_buffer_->first_pixel_output_column = LineLayout::EndOfLeftBorder; + this->fetch_line_buffer_->next_border_column = LineLayout::EndOfPixels; + this->fetch_line_buffer_->pixel_count = 256; + this->fetch_line_buffer_->screen_mode = this->screen_mode_; this->mode_timing_.maximum_visible_sprites = 4; switch(this->screen_mode_) { case ScreenMode::Text: - next_line_buffer.line_mode = LineMode::Text; - next_line_buffer.first_pixel_output_column = Timing::FirstTextCycle; - next_line_buffer.next_border_column = Timing::LastTextCycle; - next_line_buffer.pixel_count = 240; + if constexpr (is_yamaha_vdp(personality)) { + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; + } else { + this->fetch_line_buffer_->fetch_mode = FetchMode::Text; + } + this->fetch_line_buffer_->first_pixel_output_column = LineLayout::TextModeEndOfLeftBorder; + this->fetch_line_buffer_->next_border_column = LineLayout::TextModeEndOfPixels; + this->fetch_line_buffer_->pixel_count = 240; break; + case ScreenMode::YamahaText80: + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; + this->fetch_line_buffer_->first_pixel_output_column = LineLayout::TextModeEndOfLeftBorder; + this->fetch_line_buffer_->next_border_column = LineLayout::TextModeEndOfPixels; + this->fetch_line_buffer_->pixel_count = 480; + break; + case ScreenMode::SMSMode4: - next_line_buffer.line_mode = LineMode::SMS; + this->fetch_line_buffer_->fetch_mode = FetchMode::SMS; + this->mode_timing_.maximum_visible_sprites = 8; + break; + + case ScreenMode::YamahaGraphics3: + case ScreenMode::YamahaGraphics4: + case ScreenMode::YamahaGraphics7: + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; + this->mode_timing_.maximum_visible_sprites = 8; + break; + case ScreenMode::YamahaGraphics5: + case ScreenMode::YamahaGraphics6: + this->fetch_line_buffer_->pixel_count = 512; + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; this->mode_timing_.maximum_visible_sprites = 8; break; default: - next_line_buffer.line_mode = LineMode::Character; + // This covers both MultiColour and Graphics modes. + if constexpr (is_yamaha_vdp(personality)) { + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; + } else { + this->fetch_line_buffer_->fetch_mode = FetchMode::Character; + } break; } - if( - (this->screen_mode_ == ScreenMode::Blank) || - (this->write_pointer_.row >= this->mode_timing_.pixel_lines && this->write_pointer_.row != this->mode_timing_.total_lines-1)) - next_line_buffer.line_mode = LineMode::Refresh; + if constexpr (is_yamaha_vdp(personality)) { + this->fetch_line_buffer_->first_pixel_output_column += Storage::adjustment_[0]; + this->fetch_line_buffer_->next_border_column += Storage::adjustment_[0]; + } + + this->fetch_line_buffer_->vertical_state = + this->screen_mode_ == ScreenMode::Blank ? + VerticalState::Blank : + this->vertical_state(); + const bool is_refresh = this->fetch_line_buffer_->vertical_state == VerticalState::Blank; + + Storage::begin_line(this->screen_mode_, is_refresh); + + if(is_refresh) { + // The Yamaha handles refresh lines via its own microprogram; other VDPs + // can fall back on the regular refresh mechanic. + if constexpr (is_yamaha_vdp(personality)) { + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; + } else { + this->fetch_line_buffer_->fetch_mode = FetchMode::Refresh; + } + } } } #ifndef NDEBUG - assert(backup.row == this->read_pointer_.row && backup.column == this->read_pointer_.column); - backup = this->write_pointer_; + assert(backup.row == this->output_pointer_.row && backup.column == this->output_pointer_.column); + backup = this->fetch_pointer_; #endif - if(read_cycles_pool) { + if(output_cycles_pool) { // Determine how much time has passed in the remainder of this line, and proceed. - const int target_read_cycles = std::min( - Timing::CyclesPerLine - this->read_pointer_.column, - read_cycles_pool + const int target_output_cycles = std::min( + Timing::CyclesPerLine - this->output_pointer_.column, + output_cycles_pool ); - int read_cycles_performed = 0; + int output_cycles_performed = 0; uint32_t next_cram_value = 0; - while(read_cycles_performed < target_read_cycles) { - int read_cycles = target_read_cycles - read_cycles_performed; - if(!read_cycles) continue; + while(output_cycles_performed < target_output_cycles) { + int output_cycles = target_output_cycles - output_cycles_performed; + if(!output_cycles) continue; // Grab the next CRAM dot value and schedule a break in output if applicable. const uint32_t cram_value = next_cram_value; if constexpr (is_sega_vdp(personality)) { next_cram_value = 0; - if(!this->upcoming_cram_dots_.empty() && this->upcoming_cram_dots_.front().location.row == this->read_pointer_.row) { - int time_until_dot = this->upcoming_cram_dots_.front().location.column - this->read_pointer_.column; + if(!this->upcoming_cram_dots_.empty() && this->upcoming_cram_dots_.front().location.row == this->output_pointer_.row) { + int time_until_dot = this->upcoming_cram_dots_.front().location.column - this->output_pointer_.column; - if(time_until_dot < read_cycles) { - read_cycles = time_until_dot; + if(time_until_dot < output_cycles) { + output_cycles = time_until_dot; next_cram_value = this->upcoming_cram_dots_.front().value; this->upcoming_cram_dots_.erase(this->upcoming_cram_dots_.begin()); } } } - read_cycles_performed += read_cycles; + output_cycles_performed += output_cycles; - const int end_column = this->read_pointer_.column + read_cycles; - LineBuffer &line_buffer = this->line_buffers_[this->read_pointer_.row]; + const int end_column = this->output_pointer_.column + output_cycles; // -------------------- @@ -349,7 +495,7 @@ void TMS9918::run_for(const HalfCycles cycles) { #define output_default_colour_burst(x) crt_convert(output_default_colour_burst, x) #define intersect(left, right, code) { \ - const int start = std::max(this->read_pointer_.column, left); \ + const int start = std::max(this->output_pointer_.column, left); \ const int end = std::min(end_column, right); \ if(end > start) {\ code;\ @@ -358,86 +504,82 @@ void TMS9918::run_for(const HalfCycles cycles) { #define border(left, right) intersect(left, right, this->output_border(end - start, cram_value)) - if(line_buffer.line_mode == LineMode::Refresh || this->read_pointer_.row > this->mode_timing_.pixel_lines) { + const auto left_blank = [&]() { + // Blanking region: output the entire sequence when the cursor + // crosses the start-of-border point. if( - this->read_pointer_.row >= this->mode_timing_.first_vsync_line && - this->read_pointer_.row < this->mode_timing_.first_vsync_line + 4 + this->output_pointer_.column < LineLayout::EndOfLeftErase && + end_column >= LineLayout::EndOfLeftErase + ) { + output_sync(LineLayout::EndOfSync); + output_blank(LineLayout::StartOfColourBurst - LineLayout::EndOfSync); + output_default_colour_burst(LineLayout::EndOfColourBurst - LineLayout::StartOfColourBurst); + output_blank(LineLayout::EndOfLeftErase - LineLayout::EndOfColourBurst); + } + }; + + const auto right_blank = [&]() { + if(end_column == Timing::CyclesPerLine) { + output_blank(Timing::CyclesPerLine - LineLayout::EndOfRightBorder); + } + }; + + if(this->draw_line_buffer_->vertical_state != VerticalState::Pixels) { + if( + this->output_pointer_.row >= this->mode_timing_.first_vsync_line && + this->output_pointer_.row < this->mode_timing_.first_vsync_line + 4 ) { // Vertical sync. - // TODO: the Mega Drive supports interlaced video, I think? + // TODO: the Yamaha and Mega Drive both support interlaced video. if(end_column == Timing::CyclesPerLine) { output_sync(Timing::CyclesPerLine); } } else { - // Right border. - border(0, Timing::EndOfRightBorder); - - // Blanking region: output the entire sequence when the cursor - // crosses the start-of-border point. - if( - this->read_pointer_.column < Timing::StartOfLeftBorder && - end_column >= Timing::StartOfLeftBorder - ) { - output_blank(Timing::StartOfSync - Timing::EndOfRightBorder); - output_sync(Timing::EndOfSync - Timing::StartOfSync); - output_blank(Timing::StartOfColourBurst - Timing::EndOfSync); - output_default_colour_burst(Timing::EndOfColourBurst - Timing::StartOfColourBurst); - output_blank(Timing::StartOfLeftBorder - Timing::EndOfColourBurst); - } - - // Border colour for the rest of the line. - border(Timing::StartOfLeftBorder, Timing::CyclesPerLine); + left_blank(); + border(LineLayout::EndOfLeftErase, LineLayout::EndOfRightBorder); + right_blank(); } } else { - // Right border. - border(0, Timing::EndOfRightBorder); - - // Blanking region. - if( - this->read_pointer_.column < Timing::StartOfLeftBorder && - end_column >= Timing::StartOfLeftBorder - ) { - output_blank(Timing::StartOfSync - Timing::EndOfRightBorder); - output_sync(Timing::EndOfSync - Timing::StartOfSync); - output_blank(Timing::StartOfColourBurst - Timing::EndOfSync); - output_default_colour_burst(Timing::EndOfColourBurst - Timing::StartOfColourBurst); - output_blank(Timing::StartOfLeftBorder - Timing::EndOfColourBurst); - } + left_blank(); // Left border. - border(Timing::StartOfLeftBorder, line_buffer.first_pixel_output_column); + border(LineLayout::EndOfLeftErase, this->draw_line_buffer_->first_pixel_output_column); -#define draw(function, clock) { \ - const int relative_start = from_internal(start - line_buffer.first_pixel_output_column); \ - const int relative_end = from_internal(end - line_buffer.first_pixel_output_column); \ - if(relative_start == relative_end) break; \ +#define draw(function, clock) { \ + const int relative_start = from_internal(start - this->draw_line_buffer_->first_pixel_output_column); \ + const int relative_end = from_internal(end - this->draw_line_buffer_->first_pixel_output_column); \ + if(relative_start == relative_end) break; \ this->function; } // Pixel region. intersect( - line_buffer.first_pixel_output_column, - line_buffer.next_border_column, + this->draw_line_buffer_->first_pixel_output_column, + this->draw_line_buffer_->next_border_column, if(!this->asked_for_write_area_) { this->asked_for_write_area_ = true; this->pixel_origin_ = this->pixel_target_ = reinterpret_cast( - this->crt_.begin_data(line_buffer.pixel_count) + this->crt_.begin_data(size_t(this->draw_line_buffer_->pixel_count)) ); } if(this->pixel_target_) { - switch(line_buffer.line_mode) { - case LineMode::SMS: draw(draw_sms(relative_start, relative_end, cram_value), Clock::TMSPixel); break; - case LineMode::Character: draw(draw_tms_character(relative_start, relative_end), Clock::TMSPixel); break; - case LineMode::Text: draw(draw_tms_text(relative_start, relative_end), Clock::TMSPixel); break; + if constexpr (is_yamaha_vdp(personality)) { + draw(draw_yamaha(0, relative_start, relative_end), Clock::Internal); // TODO: what is the correct 'y'? + } else { + switch(this->draw_line_buffer_->fetch_mode) { + case FetchMode::SMS: draw(draw_sms(relative_start, relative_end, cram_value), Clock::TMSPixel); break; + case FetchMode::Character: draw(draw_tms_character(relative_start, relative_end), Clock::TMSPixel); break; + case FetchMode::Text: draw(template draw_tms_text(relative_start, relative_end), Clock::TMSPixel); break; - case LineMode::Refresh: break; /* Dealt with elsewhere. */ + default: break; /* Dealt with elsewhere. */ + } } } - if(end == line_buffer.next_border_column) { - const int length = line_buffer.next_border_column - line_buffer.first_pixel_output_column; - this->crt_.output_data(from_internal(length), line_buffer.pixel_count); + if(end == this->draw_line_buffer_->next_border_column) { + const int length = this->draw_line_buffer_->next_border_column - this->draw_line_buffer_->first_pixel_output_column; + this->crt_.output_data(from_internal(length), size_t(this->draw_line_buffer_->pixel_count)); this->pixel_origin_ = this->pixel_target_ = nullptr; this->asked_for_write_area_ = false; } @@ -445,10 +587,10 @@ void TMS9918::run_for(const HalfCycles cycles) { #undef draw - // Additional right border, if called for. - if(line_buffer.next_border_column != Timing::CyclesPerLine) { - border(line_buffer.next_border_column, Timing::CyclesPerLine); - } + // Right border. + border(this->draw_line_buffer_->next_border_column, LineLayout::EndOfRightBorder); + + right_blank(); } #undef border @@ -464,29 +606,32 @@ void TMS9918::run_for(const HalfCycles cycles) { // ------------- // Advance time. // ------------- - this->read_pointer_.column = end_column; + this->output_pointer_.column = end_column; + if(end_column == Timing::CyclesPerLine) { + // Advance line buffer. + this->advance(this->draw_line_buffer_); + } } - read_cycles_pool -= target_read_cycles; - if(this->read_pointer_.column == Timing::CyclesPerLine) { - this->read_pointer_.column = 0; - this->read_pointer_.row = (this->read_pointer_.row + 1) % this->mode_timing_.total_lines; + output_cycles_pool -= target_output_cycles; + if(this->output_pointer_.column == Timing::CyclesPerLine) { + this->output_pointer_.column = 0; + this->output_pointer_.row = (this->output_pointer_.row + 1) % this->mode_timing_.total_lines; } } - assert(backup.row == this->write_pointer_.row && backup.column == this->write_pointer_.column); + assert(backup.row == this->fetch_pointer_.row && backup.column == this->fetch_pointer_.column); } } template void Base::output_border(int cycles, [[maybe_unused]] uint32_t cram_dot) { cycles = from_internal(cycles); - const uint32_t border_colour = - is_sega_vdp(personality) ? - master_system_.colour_ram[16 + background_colour_] : - palette[background_colour_]; + uint32_t border_colour; if constexpr (is_sega_vdp(personality)) { + border_colour = Storage::colour_ram_[16 + background_colour_]; + if(cram_dot) { uint32_t *const pixel_target = reinterpret_cast(crt_.begin_data(1)); if(pixel_target) { @@ -498,6 +643,8 @@ void Base::output_border(int cycles, [[maybe_unused]] uint32_t cram crt_.output_level(4); cycles -= 4; } + } else { + border_colour = palette()[background_colour_]; } if(!cycles) { @@ -517,145 +664,549 @@ void Base::output_border(int cycles, [[maybe_unused]] uint32_t cram } } +// MARK: - External interface. + template -void TMS9918::write(int address, uint8_t value) { - // Writes to address 0 are writes to the video RAM. Store - // the value and return. - if(!(address & 1)) { - this->write_phase_ = false; +int Base::masked_address(int address) const { + if constexpr (is_yamaha_vdp(personality)) { + return address & 3; + } else { + return address & 1; + } +} - // Enqueue the write to occur at the next available slot. - this->read_ahead_buffer_ = value; - this->queued_access_ = MemoryAccess::Write; - this->cycles_until_access_ = Timing::VRAMAccessDelay; +template +void Base::write_vram(uint8_t value) { + write_phase_ = false; - return; + // Enqueue the write to occur at the next available slot. + read_ahead_buffer_ = value; + queued_access_ = MemoryAccess::Write; + minimum_access_column_ = fetch_pointer_.column + Timing::VRAMAccessDelay; +} + +template +void Base::commit_register(int reg, uint8_t value) { + if constexpr (is_yamaha_vdp(personality)) { + reg &= 0x3f; + } else if constexpr (is_sega_vdp(personality)) { + if(reg & 0x40) { + Storage::cram_is_selected_ = true; + return; + } + reg &= 0xf; + } else { + reg &= 0x7; } + // + // Generic TMS functionality. + // + switch(reg) { + case 0: + mode2_enable_ = value & 0x02; + break; + + case 1: + blank_display_ = !(value & 0x40); + generate_interrupts_ = value & 0x20; + mode1_enable_ = value & 0x10; + mode3_enable_ = value & 0x08; + sprites_16x16_ = value & 0x02; + sprites_magnified_ = value & 0x01; + + sprite_height_ = 8; + if(sprites_16x16_) sprite_height_ <<= 1; + if(sprites_magnified_) sprite_height_ <<= 1; + break; + + case 2: install_field<10>(pattern_name_address_, value); break; + case 3: install_field<6>(colour_table_address_, value); break; + case 4: install_field<11>(pattern_generator_table_address_, value); break; + case 5: install_field<7>(sprite_attribute_table_address_, value); break; + case 6: install_field<11>(sprite_generator_table_address_, value); break; + + case 7: + text_colour_ = value >> 4; + background_colour_ = value & 0xf; + break; + + default: break; + } + + // + // Sega extensions. + // + if constexpr (is_sega_vdp(personality)) { + switch(reg) { + default: break; + + case 0: + Storage::vertical_scroll_lock_ = value & 0x80; + Storage::horizontal_scroll_lock_ = value & 0x40; + Storage::hide_left_column_ = value & 0x20; + enable_line_interrupts_ = value & 0x10; + Storage::shift_sprites_8px_left_ = value & 0x08; + Storage::mode4_enable_ = value & 0x04; + break; + + case 2: + Storage::pattern_name_address_ = pattern_name_address_ | ((personality == TMS::SMSVDP) ? 0x000 : 0x400); + break; + + case 5: + Storage::sprite_attribute_table_address_ = sprite_attribute_table_address_ | ((personality == TMS::SMSVDP) ? 0x00 : 0x80); + break; + + case 6: + Storage::sprite_generator_table_address_ = sprite_generator_table_address_ | ((personality == TMS::SMSVDP) ? 0x0000 : 0x1800); + break; + + case 8: + Storage::horizontal_scroll_ = value; + break; + + case 9: + Storage::vertical_scroll_ = value; + break; + + case 10: + line_interrupt_target_ = value; + break; + } + } + + // + // Yamaha extensions. + // + if constexpr (is_yamaha_vdp(personality)) { + switch(reg) { + default: break; + + case 0: + Storage::mode_ = uint8_t( + (Storage::mode_ & 3) | + ((value & 0xe) << 1) + ); + enable_line_interrupts_ = value & 0x10; + + // b1–b3: M3–M5 + // b4: enable horizontal retrace interrupt + // b5: enable light pen interrupts + // b6: set colour bus to input or output mode + break; + + case 1: + Storage::mode_ = uint8_t( + (Storage::mode_ & 0x1c) | + ((value & 0x10) >> 4) | + ((value & 0x08) >> 2) + ); + break; + + case 7: + Storage::background_palette_[0] = Storage::palette_[background_colour_]; + break; + + case 8: + Storage::solid_background_ = value & 0x20; + Storage::sprites_enabled_ = !(value & 0x02); + if(value & 0x01) { + LOG("TODO: Yamaha greyscale"); + } + // b7: "1 = input on colour bus, enable mouse; 1 = output on colour bus, disable mouse" [documentation clearly in error] + // b6: 1 = enable light pen + // b5: sets the colour of code 0 to the colour of the palette (???) + // b4: 1 = colour bus in input mode; 0 = colour bus in output mode + // b3: 1 = VRAM is 64kx1 or 64kx4; 0 = 16kx1 or 16kx4; affects refresh. + // b1: 1 = disable sprites (and release sprite access slots) + // b0: 1 = output in grayscale + break; + + case 9: + mode_timing_.pixel_lines = (value & 0x80) ? 212 : 192; + mode_timing_.end_of_frame_interrupt_position.row = mode_timing_.pixel_lines+1; + // TODO: on the Yamaha, at least, tie this interrupt overtly to vertical state. + + if(value & 0x08) { + LOG("TODO: Yamaha interlace mode"); + } + + // b7: 1 = 212 lines of pixels; 0 = 192 + // b5 & b4: select simultaneous mode (seems to relate to line length and in-phase colour?) + // b3: 1 = interlace on + // b2: 1 = display two graphic screens interchangeably by even/odd field + // b1: 1 = PAL mode; 0 = NTSC mode + // b0: 1 = [dot clock] DLCLK is input; 0 = DLCLK is output + break; + + // b0–b2: A14–A16 of the colour table. + case 10: install_field<14>(colour_table_address_, value); break; + + // b0–b1: A15–A16 of the sprite table. + case 11: install_field<15>(sprite_attribute_table_address_, value); break; + + case 12: + Storage::blink_text_colour_ = value >> 4; + Storage::blink_background_colour_ = value & 0xf; + // as per register 7, but in blink mode. + break; + + case 13: + Storage::blink_periods_ = value; + if(!value) { + Storage::in_blink_ = 0; + } + + // b0–b3: display time for odd page; + // b4–b7: display time for even page. + break; + + case 14: install_field<14>(ram_pointer_, value); break; + + case 15: + Storage::selected_status_ = value & 0xf; + break; + + case 16: + Storage::palette_entry_ = value; + // b0–b3: palette entry for writing on port 2; autoincrements upon every write. + break; + + case 17: + Storage::increment_indirect_register_ = !(value & 0x80); + Storage::indirect_register_ = value & 0x3f; + break; + + case 18: + Storage::adjustment_[0] = (8 - ((value & 15) ^ 8)) * 4; + Storage::adjustment_[1] = 8 - ((value >> 4) ^ 8); + // b0-b3: horizontal adjustment + // b4-b7: vertical adjustment + break; + + case 19: + line_interrupt_target_ = value; + // b0–b7: line to match for interrupts (if eabled) + break; + + case 20: + case 21: + case 22: +// LOG("TODO: Yamaha colour burst selection; " << PADHEX(2) << +value); + // Documentation is "fill with 0s for no colour burst; magic pattern for colour burst" + break; + + case 23: + Storage::vertical_offset_ = value; + break; + + case 32: Storage::command_context_.source.template set<0, false>(value); break; + case 33: Storage::command_context_.source.template set<0, true>(value); break; + case 34: Storage::command_context_.source.template set<1, false>(value); break; + case 35: Storage::command_context_.source.template set<1, true>(value); break; + + case 36: Storage::command_context_.destination.template set<0, false>(value); break; + case 37: Storage::command_context_.destination.template set<0, true>(value); break; + case 38: Storage::command_context_.destination.template set<1, false>(value); break; + case 39: Storage::command_context_.destination.template set<1, true>(value); break; + + case 40: Storage::command_context_.size.template set<0, false>(value); break; + case 41: Storage::command_context_.size.template set<0, true>(value); break; + case 42: Storage::command_context_.size.template set<1, false>(value); break; + case 43: Storage::command_context_.size.template set<1, true>(value); break; + + case 44: + Storage::command_context_.colour.set(value); + + // Check whether a command was blocked on this. + if( + Storage::command_ && + Storage::command_->access == Command::AccessType::WaitForColourReceipt + ) { + Storage::command_->advance(); + Storage::update_command_step(fetch_pointer_.column); + } + break; + + case 45: + Storage::command_context_.arguments = value; + // b6: MXC, i.e. destination for INed/OUTed video data; 0 = video RAM; 1 = expansion RAM. + // b5: MXD, destination for command engine. + // b4: MXS, source for command engine. + // b3: DIY + // b2: DIX + // b1: EQ + // b0: MAJ + break; + + case 46: + // b0–b3: LO0–LO3 (i.e. operation to apply if this is a logical command) + // b4–b7: CM0-CM3 (i.e. command to perform) + + // If a command is already ongoing and this is not a stop, ignore it. + if(Storage::command_ && (value >> 4) != 0b0000) { + break; + } + +#define Begin(x) Storage::command_ = std::make_unique(Storage::command_context_, Storage::mode_description_); + using MoveType = Commands::MoveType; + switch(value >> 4) { + // All codes not listed below are invalid; treat them as STOP. + default: + case 0b0000: Storage::command_ = nullptr; break; // STOP. + + case 0b0100: Begin(Point); break; // POINT [read a pixel colour]. + case 0b0101: Begin(Point); break; // PSET [plot a pixel]. + case 0b0110: break; // TODO: srch. [search horizontally for a colour] + case 0b0111: Begin(Line); break; // LINE [draw a Bresenham line]. + + case 0b1000: Begin(Fill); break; // LMMV [logical move, VDP to VRAM, i.e. solid-colour fill]. + case 0b1001: Begin(Move); break; // LMMM [logical move, VRAM to VRAM]. + case 0b1010: break; // TODO: lmcm. [logical move, VRAM to CPU] + case 0b1011: Begin(MoveFromCPU); break; // LMMC [logical move, CPU to VRAM]. + + case 0b1100: Begin(Fill); break; // HMMV [high-speed move, VDP to VRAM, i.e. single-byte fill]. + case 0b1101: Begin(Move); break; // HMMM [high-speed move, VRAM to VRAM]. + case 0b1110: Begin(Move); break; // YMMM [high-speed move, y only, VRAM to VRAM]. + case 0b1111: Begin(MoveFromCPU); break; // HMMC [high-speed move, CPU to VRAM]. + } +#undef Begin + + Storage::command_context_.pixel_operation = CommandContext::LogicalOperation(value & 7); + Storage::command_context_.test_source = value & 8; + + // Kill the command immediately if it's done in zero operations + // (e.g. a line of length 0). + if(!Storage::command_ && (value >> 4)) { + LOG("TODO: Yamaha command " << PADHEX(2) << +value); + } + + // Seed timing information if a command was found. + Storage::update_command_step(fetch_pointer_.column); + break; + } + } +} + +template +void Base::write_register(uint8_t value) { // Writes to address 1 are performed in pairs; if this is the // low byte of a value, store it and wait for the high byte. - if(!this->write_phase_) { - this->low_write_ = value; - this->write_phase_ = true; + if(!write_phase_) { + low_write_ = value; + write_phase_ = true; - // The initial write should half update the access pointer. - this->ram_pointer_ = (this->ram_pointer_ & 0xff00) | this->low_write_; + // The initial write should half update the access pointer, other than + // on the Yamaha. + if constexpr (!is_yamaha_vdp(personality)) { + install_field<0>(ram_pointer_, value); + } return; } // The RAM pointer is always set on a second write, regardless of // whether the caller is intending to enqueue a VDP operation. - this->ram_pointer_ = (this->ram_pointer_ & 0x00ff) | uint16_t(value << 8); + // If this isn't a Yamaha VDP then the RAM address is updated + // regardless of whether this turns out to be a register access. + // + // The top two bits are used to determine the type of write; only + // the lower six are actual address. + if constexpr (!is_yamaha_vdp(personality)) { + install_field<8, 6>(ram_pointer_, value); + } - this->write_phase_ = false; + write_phase_ = false; if(value & 0x80) { - if constexpr (is_sega_vdp(personality)) { - if(value & 0x40) { - this->master_system_.cram_is_selected = true; - return; - } - value &= 0xf; - } else { - value &= 0x7; - } - - // This is a write to a register. - switch(value) { - case 0: - if constexpr (is_sega_vdp(personality)) { - this->master_system_.vertical_scroll_lock = this->low_write_ & 0x80; - this->master_system_.horizontal_scroll_lock = this->low_write_ & 0x40; - this->master_system_.hide_left_column = this->low_write_ & 0x20; - this->enable_line_interrupts_ = this->low_write_ & 0x10; - this->master_system_.shift_sprites_8px_left = this->low_write_ & 0x08; - this->master_system_.mode4_enable = this->low_write_ & 0x04; - } - this->mode2_enable_ = this->low_write_ & 0x02; - break; - - case 1: - this->blank_display_ = !(this->low_write_ & 0x40); - this->generate_interrupts_ = this->low_write_ & 0x20; - this->mode1_enable_ = this->low_write_ & 0x10; - this->mode3_enable_ = this->low_write_ & 0x08; - this->sprites_16x16_ = this->low_write_ & 0x02; - this->sprites_magnified_ = this->low_write_ & 0x01; - - this->sprite_height_ = 8; - if(this->sprites_16x16_) this->sprite_height_ <<= 1; - if(this->sprites_magnified_) this->sprite_height_ <<= 1; - break; - - case 2: - this->pattern_name_address_ = size_t((this->low_write_ & 0xf) << 10) | 0x3ff; - this->master_system_.pattern_name_address = this->pattern_name_address_ | ((personality == TMS::SMSVDP) ? 0x000 : 0x400); - break; - - case 3: - this->colour_table_address_ = size_t(this->low_write_ << 6) | 0x3f; - break; - - case 4: - this->pattern_generator_table_address_ = size_t((this->low_write_ & 0x07) << 11) | 0x7ff; - break; - - case 5: - this->sprite_attribute_table_address_ = size_t((this->low_write_ & 0x7f) << 7) | 0x7f; - this->master_system_.sprite_attribute_table_address = this->sprite_attribute_table_address_ | ((personality == TMS::SMSVDP) ? 0x00 : 0x80); - break; - - case 6: - this->sprite_generator_table_address_ = size_t((this->low_write_ & 0x07) << 11) | 0x7ff; - this->master_system_.sprite_generator_table_address = this->sprite_generator_table_address_ | ((personality == TMS::SMSVDP) ? 0x0000 : 0x1800); - break; - - case 7: - this->text_colour_ = this->low_write_ >> 4; - this->background_colour_ = this->low_write_ & 0xf; - break; - - case 8: - if constexpr (is_sega_vdp(personality)) { - this->master_system_.horizontal_scroll = this->low_write_; - } - break; - - case 9: - if constexpr (is_sega_vdp(personality)) { - this->master_system_.vertical_scroll = this->low_write_; - } - break; - - case 10: - if constexpr (is_sega_vdp(personality)) { - this->line_interrupt_target = this->low_write_; - } - break; - - default: - LOG("Unknown TMS write: " << int(this->low_write_) << " to " << int(value)); - break; - } + commit_register(value, low_write_); } else { - // This is an access via the RAM pointer. + // This is an access via the RAM pointer; if this is a Yamaha VDP then update + // the low 14-bits of the RAM pointer now. + if constexpr (is_yamaha_vdp(personality)) { + install_field<0>(ram_pointer_, low_write_); + install_field<8, 6>(ram_pointer_, value); + } + if(!(value & 0x40)) { // A read request is enqueued upon setting the address; conversely a write // won't be enqueued unless and until some actual data is supplied. - this->queued_access_ = MemoryAccess::Read; - this->cycles_until_access_ = Timing::VRAMAccessDelay; + queued_access_ = MemoryAccess::Read; + minimum_access_column_ = fetch_pointer_.column + Timing::VRAMAccessDelay; + } + + if constexpr (is_sega_vdp(personality)) { + Storage::cram_is_selected_ = false; } - this->master_system_.cram_is_selected = false; } } +template +void Base::write_palette(uint8_t value) { + if constexpr (is_yamaha_vdp(personality)) { + if(!Storage::palette_write_phase_) { + Storage::new_colour_ = value; + Storage::palette_write_phase_ = true; + return; + } + + Storage::palette_write_phase_ = false; + + const uint8_t r = ((Storage::new_colour_ >> 4) & 7) * 255 / 7; + const uint8_t g = (value & 7) * 255 / 7; + const uint8_t b = (Storage::new_colour_ & 7) * 255 / 7; + + Storage::palette_[Storage::palette_entry_ & 0xf] = palette_pack(r, g, b); + Storage::background_palette_[Storage::palette_entry_ & 0xf] = palette_pack(r, g, b); + Storage::background_palette_[0] = Storage::palette_[background_colour_]; + + ++Storage::palette_entry_; + } +} + +template +void Base::write_register_indirect([[maybe_unused]] uint8_t value) { + if constexpr (is_yamaha_vdp(personality)) { + // Register 17 cannot be written to indirectly. + if(Storage::indirect_register_ != 17) { + commit_register(Storage::indirect_register_, value); + } + Storage::indirect_register_ += Storage::increment_indirect_register_; + } +} + +template +void TMS9918::write(int address, uint8_t value) { + switch(this->masked_address(address)) { + default: break; + case 0: this->write_vram(value); break; + case 1: this->write_register(value); break; + case 2: this->write_palette(value); break; + case 3: this->write_register_indirect(value); break; + } +} + +template +uint8_t Base::read_vram() { + // Take whatever is currently in the read-ahead buffer and + // enqueue a further read to occur at the next available slot. + const uint8_t result = read_ahead_buffer_; + queued_access_ = MemoryAccess::Read; + return result; +} + +template +uint8_t Base::read_register() { + if constexpr (is_yamaha_vdp(personality)) { + switch(Storage::selected_status_) { + default: + case 0: break; + + case 1: { + // b7 = light pen; set when light is detected, reset on read; + // or: mouse button 2 currently down. + // b6 = light pen button or mouse button 1. + // b5–b1 = VDP identification (0 = 9938; 2 = 9958) + // b0 = set when the VDP reaches the line provided in the line interrupt register. + // Reset upon read. + const uint8_t result = + (personality == Personality::V9938 ? 0x0 : 0x4) | + (line_interrupt_pending_ ? 0x01 : 0x00); + + line_interrupt_pending_ = false; + return result; + } break; + + case 2: { + // b7 = transfer ready flag (i.e. VDP ready for next transfer) + // b6 = 1 during vblank + // b5 = 1 during hblank + // b4 = set if colour detected during search command + // b1 = display field odd/even + // b0 = command ongoing + const uint8_t transfer_ready = + (queued_access_ == MemoryAccess::None ? 0x80 : 0x00) & + (( + !Storage::command_ || + !Storage::command_->is_cpu_transfer || + Storage::command_->access == Command::AccessType::WaitForColourReceipt + ) ? 0x80 : 0x00); + + return + transfer_ready | + (vertical_state() != VerticalState::Pixels ? 0x40 : 0x00) | + (is_horizontal_blank() ? 0x20 : 0x00) | + (Storage::command_ ? 0x01 : 0x00); + + } break; + + case 3: return uint8_t(Storage::collision_location_[0]); + case 4: return uint8_t((Storage::collision_location_[0] >> 8) | 0xfe); + case 5: return uint8_t(Storage::collision_location_[1]); + case 6: return uint8_t((Storage::collision_location_[1] >> 8) | 0xfc); + + case 7: return Storage::colour_status_; + + case 8: return uint8_t(Storage::colour_location_); + case 9: return uint8_t((Storage::colour_location_ >> 8) | 0xfe); + } + } + + // Gets the status register. + const uint8_t result = status_; + status_ &= ~(StatusInterrupt | StatusSpriteOverflow | StatusSpriteCollision); + if constexpr (is_sega_vdp(personality)) { + line_interrupt_pending_ = false; + } + return result; +} + +template +uint8_t TMS9918::read(int address) { + const int target = this->masked_address(address); + + if(target < 2) { + this->write_phase_ = false; + } + + switch(target) { + default: return 0xff; + case 0: return this->read_vram(); + case 1: return this->read_register(); + } +} + +// MARK: - Ephemeral state. + +template +int Base::fetch_line() const { + // This is the proper Master System value; TODO: what's correct for Yamaha, etc? + constexpr int row_change_position = 31; + + return + (this->fetch_pointer_.column < row_change_position) + ? (this->fetch_pointer_.row + this->mode_timing_.total_lines - 1) % this->mode_timing_.total_lines + : this->fetch_pointer_.row; +} + +template +VerticalState Base::vertical_state() const { + if(vertical_active_) { + return VerticalState::Pixels; + } else if(fetch_pointer_.row == mode_timing_.total_lines - 1) { + return VerticalState::Prefetch; + } else { + return VerticalState::Blank; + } +} + +template +bool Base::is_horizontal_blank() const { + return fetch_pointer_.column < LineLayout::EndOfLeftErase || fetch_pointer_.column >= LineLayout::EndOfRightBorder; +} + template uint8_t TMS9918::get_current_line() const { - // Determine the row to return. - constexpr int row_change_position = 63; // This is the proper Master System value; substitute if any other VDPs turn out to have this functionality. - int source_row = - (this->write_pointer_.column < row_change_position) - ? (this->write_pointer_.row + this->mode_timing_.total_lines - 1) % this->mode_timing_.total_lines - : this->write_pointer_.row; + int source_row = this->fetch_line(); if(this->tv_standard_ == TVStandard::NTSC) { if(this->mode_timing_.pixel_lines == 240) { @@ -682,26 +1233,6 @@ uint8_t TMS9918::get_current_line() const { return uint8_t(source_row); } - -template -uint8_t TMS9918::read(int address) { - this->write_phase_ = false; - - // Reads from address 0 read video RAM, via the read-ahead buffer. - if(!(address & 1)) { - // Enqueue the write to occur at the next available slot. - const uint8_t result = this->read_ahead_buffer_; - this->queued_access_ = MemoryAccess::Read; - return result; - } - - // Reads from address 1 get the status register. - const uint8_t result = this->status_; - this->status_ &= ~(StatusInterrupt | StatusSpriteOverflow | StatusSpriteCollision); - this->line_interrupt_pending_ = false; - return result; -} - template HalfCycles TMS9918::get_next_sequence_point() const { if(!this->generate_interrupts_ && !this->enable_line_interrupts_) return HalfCycles::max(); @@ -712,7 +1243,7 @@ HalfCycles TMS9918::get_next_sequence_point() const { int time_until_frame_interrupt = ( ((this->mode_timing_.end_of_frame_interrupt_position.row * Timing::CyclesPerLine) + this->mode_timing_.end_of_frame_interrupt_position.column + frame_length) - - ((this->write_pointer_.row * Timing::CyclesPerLine) + this->write_pointer_.column) + ((this->fetch_pointer_.row * Timing::CyclesPerLine) + this->fetch_pointer_.column) ) % frame_length; if(!time_until_frame_interrupt) time_until_frame_interrupt = frame_length; @@ -723,8 +1254,8 @@ HalfCycles TMS9918::get_next_sequence_point() const { // Calculate when the next line interrupt will occur. int next_line_interrupt_row = -1; - int cycles_to_next_interrupt_threshold = this->mode_timing_.line_interrupt_position - this->write_pointer_.column; - int line_of_next_interrupt_threshold = this->write_pointer_.row; + int cycles_to_next_interrupt_threshold = this->mode_timing_.line_interrupt_position - this->fetch_pointer_.column; + int line_of_next_interrupt_threshold = this->fetch_pointer_.row; if(cycles_to_next_interrupt_threshold <= 0) { cycles_to_next_interrupt_threshold += Timing::CyclesPerLine; ++line_of_next_interrupt_threshold; @@ -734,14 +1265,18 @@ HalfCycles TMS9918::get_next_sequence_point() const { // If there is still time for a line interrupt this frame, that'll be it; // otherwise it'll be on the next frame, supposing there's ever time for // it at all. - if(line_of_next_interrupt_threshold + this->line_interrupt_counter <= this->mode_timing_.pixel_lines) { - next_line_interrupt_row = line_of_next_interrupt_threshold + this->line_interrupt_counter; + if(line_of_next_interrupt_threshold + this->line_interrupt_counter_ <= this->mode_timing_.pixel_lines) { + next_line_interrupt_row = line_of_next_interrupt_threshold + this->line_interrupt_counter_; } else { - if(this->line_interrupt_target <= this->mode_timing_.pixel_lines) - next_line_interrupt_row = this->mode_timing_.total_lines + this->line_interrupt_target; + if(this->line_interrupt_target_ <= this->mode_timing_.pixel_lines) + next_line_interrupt_row = this->mode_timing_.total_lines + this->line_interrupt_target_; } } + if constexpr (is_yamaha_vdp(personality)) { + next_line_interrupt_row = (this->line_interrupt_target_ - Storage::vertical_offset_) & 0xff; + } + // 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) { @@ -752,7 +1287,8 @@ HalfCycles TMS9918::get_next_sequence_point() const { // Figure out the number of internal cycles until the next line interrupt, which is the amount // of time to the next tick over and then next_line_interrupt_row - row_ lines further. - const int local_cycles_until_line_interrupt = cycles_to_next_interrupt_threshold + (next_line_interrupt_row - line_of_next_interrupt_threshold) * Timing::CyclesPerLine; + const int lines_until_interrupt = (next_line_interrupt_row - line_of_next_interrupt_threshold + this->mode_timing_.total_lines) % this->mode_timing_.total_lines; + const int local_cycles_until_line_interrupt = cycles_to_next_interrupt_threshold + lines_until_interrupt * Timing::CyclesPerLine; if(!this->generate_interrupts_) return this->clock_converter_.half_cycles_before_internal_cycles(local_cycles_until_line_interrupt); // Return whichever interrupt is closer. @@ -763,8 +1299,8 @@ template HalfCycles TMS9918::get_time_until_line(int line) { if(line < 0) line += this->mode_timing_.total_lines; - int cycles_to_next_interrupt_threshold = this->mode_timing_.line_interrupt_position - this->write_pointer_.column; - int line_of_next_interrupt_threshold = this->write_pointer_.row; + int cycles_to_next_interrupt_threshold = this->mode_timing_.line_interrupt_position - this->fetch_pointer_.column; + int line_of_next_interrupt_threshold = this->fetch_pointer_.row; if(cycles_to_next_interrupt_threshold <= 0) { cycles_to_next_interrupt_threshold += Timing::CyclesPerLine; ++line_of_next_interrupt_threshold; @@ -784,26 +1320,25 @@ bool TMS9918::get_interrupt_line() const { (this->enable_line_interrupts_ && this->line_interrupt_pending_); } -// TODO: [potentially] remove Master System timing assumptions in latch and get_latched below. +// TODO: [potentially] remove Master System timing assumptions in latch and get_latched below, if any other VDP uses these calls. template uint8_t TMS9918::get_latched_horizontal_counter() const { - // Translate from internal numbering, which puts pixel output - // in the final 256 pixels of 342, to the public numbering, + // Translate from internal numbering to the public numbering, // which counts the 256 pixels as items 0–255, starts // counting at -48, and returns only the top 8 bits of the number. - int public_counter = this->latched_column_ - (342 - 256); - if(public_counter < -46) public_counter += 342; + int public_counter = this->latched_column_ - LineLayout::EndOfLeftBorder; + if(public_counter < -46) public_counter += Timing::CyclesPerLine; return uint8_t(public_counter >> 1); } template void TMS9918::latch_horizontal_counter() { - this->latched_column_ = this->write_pointer_.column; + this->latched_column_ = this->fetch_pointer_.column; } template class TI::TMS::TMS9918; template class TI::TMS::TMS9918; -template class TI::TMS::TMS9918; +//template class TI::TMS::TMS9918; template class TI::TMS::TMS9918; template class TI::TMS::TMS9918; -template class TI::TMS::TMS9918; -template class TI::TMS::TMS9918; +//template class TI::TMS::TMS9918; +//template class TI::TMS::TMS9918; diff --git a/Components/9918/Implementation/9918Base.hpp b/Components/9918/Implementation/9918Base.hpp index 6c4bef30c..5e63ae707 100644 --- a/Components/9918/Implementation/9918Base.hpp +++ b/Components/9918/Implementation/9918Base.hpp @@ -15,7 +15,11 @@ #include "../../../Numeric/BitReverse.hpp" #include "../../../Outputs/CRT/CRT.hpp" +#include "AccessEnums.hpp" +#include "LineBuffer.hpp" #include "PersonalityTraits.hpp" +#include "Storage.hpp" +#include "YamahaCommands.hpp" #include #include @@ -24,93 +28,7 @@ #include #include -namespace TI { -namespace TMS { - -// The screen mode is a necessary predecessor to picking the line mode, -// which is the thing latched per line. -enum class ScreenMode { - Blank, - Text, - MultiColour, - ColouredText, - Graphics, - SMSMode4 -}; - -enum class LineMode { - Text, - Character, - Refresh, - SMS -}; - -enum class MemoryAccess { - Read, Write, None -}; - -// Temporary buffers collect a representation of each line prior to pixel serialisation. -struct LineBuffer { - // The line mode describes the proper timing diagram for this line. - LineMode line_mode = LineMode::Text; - - // 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 = 0; - uint8_t flags = 0; - } names[40]; - - // The patterns array holds tile patterns, corresponding 1:1 with names. - // Four bytes per pattern is the maximum required by any - // currently-implemented VDP. - uint8_t patterns[40][4]; - - /* - Horizontal layout (on a 342-cycle clock): - - 15 cycles right border - 58 cycles blanking & sync - 13 cycles left border - - ... i.e. to cycle 86, then: - - border up to first_pixel_output_column; - pixels up to next_border_column; - border up to the end. - - e.g. standard 256-pixel modes will want to set - first_pixel_output_column = 86, next_border_column = 342. - */ - int first_pixel_output_column = 94; - int next_border_column = 334; - size_t pixel_count = 256; - - // An active sprite is one that has been selected for composition onto - // this line. - struct ActiveSprite { - int index = 0; // The original in-table index of this sprite. - int row = 0; // The row of the sprite that should be drawn. - int x = 0; // The sprite's x position on screen. - - uint8_t image[4]; // Up to four bytes of image information. - int shift_position = 0; // An offset representing how much of the image information has already been drawn. - } active_sprites[8]; - - int active_sprite_slot = 0; // A pointer to the slot into which a new active sprite will be deposited, if required. - bool sprites_stopped = false; // A special TMS feature is that a sentinel value can be used to prevent any further sprites - // being evaluated for display. This flag determines whether the sentinel has yet been reached. - - void reset_sprite_collection(); -}; - -struct LineBufferPointer { - int row, column; -}; +namespace TI::TMS { constexpr uint8_t StatusInterrupt = 0x80; constexpr uint8_t StatusSpriteOverflow = 0x40; @@ -118,7 +36,7 @@ constexpr uint8_t StatusSpriteOverflow = 0x40; constexpr int StatusSpriteCollisionShift = 5; constexpr uint8_t StatusSpriteCollision = 0x20; -template struct Base { +template struct Base: public Storage { Base(); static constexpr int output_lag = 11; // i.e. pixel output will occur 11 cycles @@ -133,7 +51,7 @@ template struct Base { } // The default TMS palette. - static constexpr std::array palette { + static constexpr std::array default_palette { palette_pack(0, 0, 0), palette_pack(0, 0, 0), palette_pack(33, 200, 66), @@ -154,9 +72,29 @@ template struct Base { palette_pack(204, 204, 204), palette_pack(255, 255, 255) }; + const std::array &palette() { + if constexpr (is_yamaha_vdp(personality)) { + return Storage::solid_background_ ? Storage::palette_ : Storage::background_palette_; + } + return default_palette; + } Outputs::CRT::CRT crt_; TVStandard tv_standard_ = TVStandard::NTSC; + using AddressT = typename Storage::AddressT; + + /// Mutates @c target such that @c source replaces the @c length bits that currently start + /// at bit @c shift . Subsequently ensures @c target is constrained by the + /// applicable @c memory_mask. + template void install_field(AddressT &target, uint8_t source) { + static_assert(length > 0 && length <= 8); + constexpr auto source_mask = (1 << length) - 1; + constexpr auto mask = AddressT(~(source_mask << shift)); + target = ( + (target & mask) | + AddressT((source & source_mask) << shift) + ) & memory_mask(personality); + } // Personality-specific metrics and converters. ClockConverter clock_converter_; @@ -165,10 +103,9 @@ template struct Base { std::array ram_; // State of the DRAM/CRAM-access mechanism. - uint16_t ram_pointer_ = 0; + AddressT ram_pointer_ = 0; uint8_t read_ahead_buffer_ = 0; MemoryAccess queued_access_ = MemoryAccess::None; - int cycles_until_access_ = 0; int minimum_access_column_ = 0; // The main status register. @@ -186,14 +123,25 @@ template struct Base { bool sprites_16x16_ = false; bool sprites_magnified_ = false; bool generate_interrupts_ = false; - int sprite_height_ = 8; + uint8_t sprite_height_ = 8; // Programmer-specified addresses. - size_t pattern_name_address_ = 0; // i.e. address of the tile map. - size_t colour_table_address_ = 0; // address of the colour map (if applicable). - size_t pattern_generator_table_address_ = 0; // address of the tile contents. - size_t sprite_attribute_table_address_ = 0; // address of the sprite list. - size_t sprite_generator_table_address_ = 0; // address of the sprite contents. + // + // The TMS and descendants combine various parts of the address with AND operations, + // e.g. the fourth byte in the pattern name table will be at `pattern_name_address_ & 4`; + // ordinarily the difference between that and plain substitution is invisible because + // the programmer mostly can't set low-enough-order bits. That's not universally true + // though, so this implementation uses AND throughout. + // + // ... therefore, all programmer-specified addresses are seeded as all '1's. As and when + // actual addresses are specified, the relevant bits will be substituted in. + // + // Cf. install_field. + AddressT pattern_name_address_ = memory_mask(personality); // Address of the tile map. + AddressT colour_table_address_ = memory_mask(personality); // Address of the colour map (if applicable). + AddressT pattern_generator_table_address_ = memory_mask(personality); // Address of the tile contents. + AddressT sprite_attribute_table_address_ = memory_mask(personality); // Address of the sprite list. + AddressT sprite_generator_table_address_ = memory_mask(personality); // Address of the sprite contents. // Default colours. uint8_t text_colour_ = 0; @@ -224,79 +172,117 @@ template struct Base { // Set the position, in cycles, of the two interrupts, // within a line. + // + // TODO: redetermine where this number came from. struct { - int column = 4; - int row = 193; + int column = 313; + int row = 192; } end_of_frame_interrupt_position; int line_interrupt_position = -1; // Enables or disabled the recognition of the sprite // list terminator, and sets the terminator value. bool allow_sprite_terminator = true; - uint8_t sprite_terminator = 0xd0; + uint8_t sprite_terminator(ScreenMode mode) { + switch(mode) { + default: return 0xd0; + case ScreenMode::YamahaGraphics3: + case ScreenMode::YamahaGraphics4: + case ScreenMode::YamahaGraphics5: + case ScreenMode::YamahaGraphics6: + case ScreenMode::YamahaGraphics7: + return 0xd8; + } + } } mode_timing_; - uint8_t line_interrupt_target = 0xff; - uint8_t line_interrupt_counter = 0; + uint8_t line_interrupt_target_ = 0xff; + uint8_t line_interrupt_counter_ = 0; bool enable_line_interrupts_ = false; bool line_interrupt_pending_ = false; + bool vertical_active_ = false; - ScreenMode screen_mode_; - LineBuffer line_buffers_[313]; - void posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_y, int screen_row); + ScreenMode screen_mode_, underlying_mode_; + + using LineBufferArray = std::array; + LineBufferArray line_buffers_; + LineBufferArray::iterator fetch_line_buffer_; + LineBufferArray::iterator draw_line_buffer_; + void advance(LineBufferArray::iterator &iterator) { + ++iterator; + if(iterator == line_buffers_.end()) { + iterator = line_buffers_.begin(); + } + } + + using SpriteBufferArray = std::array; + SpriteBufferArray sprite_buffers_; + SpriteBufferArray::iterator fetch_sprite_buffer_; + SpriteBuffer *fetched_sprites_ = nullptr; + void advance(SpriteBufferArray::iterator &iterator) { + ++iterator; + if(iterator == sprite_buffers_.end()) { + iterator = sprite_buffers_.begin(); + } + } + void regress(SpriteBufferArray::iterator &iterator) { + if(iterator == sprite_buffers_.begin()) { + iterator = sprite_buffers_.end(); + } + --iterator; + } + + AddressT tile_offset_ = 0; + uint8_t name_[4]{}; + void posit_sprite(int sprite_number, int sprite_y, uint8_t 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. - LineBufferPointer read_pointer_, write_pointer_; + LineBufferPointer output_pointer_, fetch_pointer_; - // The SMS VDP has a programmer-set colour palette, with a dedicated patch of RAM. But the RAM is only exactly - // fast enough for the pixel clock. So when the programmer writes to it, that causes a one-pixel glitch; there - // isn't the bandwidth for the read both write to occur simultaneously. The following buffer therefore keeps - // track of pending collisions, for visual reproduction. - struct CRAMDot { - LineBufferPointer location; - uint32_t value; - }; - std::vector upcoming_cram_dots_; + int fetch_line() const; + bool is_horizontal_blank() const; + VerticalState vertical_state() const; - // 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; + int masked_address(int address) const; + void write_vram(uint8_t); + void write_register(uint8_t); + void write_palette(uint8_t); + void write_register_indirect(uint8_t); + uint8_t read_vram(); + uint8_t read_register(); - // The Master System's additional colour RAM. - uint32_t colour_ram[32]; - bool cram_is_selected = false; + void commit_register(int reg, uint8_t value); - // Holds the vertical scroll position for this frame; this is latched - // once and cannot dynamically be changed until the next frame. - uint8_t latched_vertical_scroll = 0; - - size_t pattern_name_address; - size_t sprite_attribute_table_address; - size_t sprite_generator_table_address; - } master_system_; - - ScreenMode current_screen_mode() const { - if(blank_display_) { + template ScreenMode current_screen_mode() const { + if(check_blank && blank_display_) { return ScreenMode::Blank; } if constexpr (is_sega_vdp(personality)) { - if(master_system_.mode4_enable) { + if(Storage::mode4_enable_) { return ScreenMode::SMSMode4; } } + if constexpr (is_yamaha_vdp(personality)) { + switch(Storage::mode_) { + case 0b00001: return ScreenMode::Text; + case 0b01001: return ScreenMode::YamahaText80; + case 0b00010: return ScreenMode::MultiColour; + case 0b00000: return ScreenMode::YamahaGraphics1; + case 0b00100: return ScreenMode::YamahaGraphics2; + case 0b01000: return ScreenMode::YamahaGraphics3; + case 0b01100: return ScreenMode::YamahaGraphics4; + case 0b10000: return ScreenMode::YamahaGraphics5; + case 0b10100: return ScreenMode::YamahaGraphics6; + case 0b11100: return ScreenMode::YamahaGraphics7; + } + } + if(!mode1_enable_ && !mode2_enable_ && !mode3_enable_) { return ScreenMode::ColouredText; } @@ -317,24 +303,252 @@ template struct Base { return ScreenMode::Blank; } + static AddressT rotate(AddressT address) { + return AddressT((address >> 1) | (address << 16)) & memory_mask(personality); + } + + AddressT command_address(Vector location, bool expansion) const { + if constexpr (is_yamaha_vdp(personality)) { + switch(this->underlying_mode_) { + default: + case ScreenMode::YamahaGraphics4: // 256 pixels @ 4bpp + return AddressT( + ((location.v[0] >> 1) & 127) + + (location.v[1] << 7) + ); + + case ScreenMode::YamahaGraphics5: // 512 pixels @ 2bpp + return AddressT( + ((location.v[0] >> 2) & 127) + + (location.v[1] << 7) + ); + + case ScreenMode::YamahaGraphics6: { // 512 pixels @ 4bpp + const auto linear_address = + AddressT( + ((location.v[0] >> 1) & 255) + + (location.v[1] << 8) + ); + return expansion ? linear_address : rotate(linear_address); + } + + case ScreenMode::YamahaGraphics7: { // 256 pixels @ 8bpp + const auto linear_address = + AddressT( + ((location.v[0] >> 0) & 255) + + (location.v[1] << 8) + ); + return expansion ? linear_address : rotate(linear_address); + } + } + } else { + return 0; + } + } + + uint8_t extract_colour(uint8_t byte, Vector location) const { + switch(this->screen_mode_) { + default: + case ScreenMode::YamahaGraphics4: // 256 pixels @ 4bpp + case ScreenMode::YamahaGraphics6: // 512 pixels @ 4bpp + return (byte >> (((location.v[0] & 1) ^ 1) << 2)) & 0xf; + + case ScreenMode::YamahaGraphics5: // 512 pixels @ 2bpp + return (byte >> (((location.v[0] & 3) ^ 3) << 1)) & 0x3; + + case ScreenMode::YamahaGraphics7: // 256 pixels @ 8bpp + return byte; + } + } + + std::pair command_colour_mask(Vector location) const { + if constexpr (is_yamaha_vdp(personality)) { + auto &context = Storage::command_context_; + auto colour = context.latched_colour.has_value() ? context.latched_colour : context.colour; + + switch(this->screen_mode_) { + default: + case ScreenMode::YamahaGraphics4: // 256 pixels @ 4bpp + case ScreenMode::YamahaGraphics6: // 512 pixels @ 4bpp + return + std::make_pair( + 0xf0 >> ((location.v[0] & 1) << 2), + colour.colour4bpp + ); + + case ScreenMode::YamahaGraphics5: // 512 pixels @ 2bpp + return + std::make_pair( + 0xc0 >> ((location.v[0] & 3) << 1), + colour.colour2bpp + ); + + case ScreenMode::YamahaGraphics7: // 256 pixels @ 8bpp + return + std::make_pair( + 0xff, + colour.colour + ); + } + } else { + return std::make_pair(0, 0); + } + } + void do_external_slot(int access_column) { // Don't do anything if the required time for the access to become executable // has yet to pass. - if(access_column < minimum_access_column_) { + if(queued_access_ == MemoryAccess::None || access_column < minimum_access_column_) { + if constexpr (is_yamaha_vdp(personality)) { + using CommandStep = typename Storage::CommandStep; + + if( + Storage::next_command_step_ == CommandStep::None || + access_column < Storage::minimum_command_column_ + ) { + return; + } + + auto &context = Storage::command_context_; + const uint8_t *const source = (context.arguments & 0x10) ? Storage::expansion_ram_.data() : ram_.data(); + const AddressT source_mask = (context.arguments & 0x10) ? 0xfff : 0x1ffff; + uint8_t *const destination = (context.arguments & 0x20) ? Storage::expansion_ram_.data() : ram_.data(); + const AddressT destination_mask = (context.arguments & 0x20) ? 0xfff : 0x1ffff; + switch(Storage::next_command_step_) { + // Duplicative, but keeps the compiler happy. + case CommandStep::None: + break; + + case CommandStep::CopySourcePixelToStatus: + Storage::colour_status_ = + extract_colour( + source[command_address(context.source, context.arguments & 0x10) & source_mask], + context.source + ); + + Storage::command_->advance(); + Storage::update_command_step(access_column); + break; + + case CommandStep::ReadSourcePixel: + context.latched_colour.set( + extract_colour( + source[command_address(context.source, context.arguments & 0x10)] & source_mask, + context.source) + ); + + Storage::minimum_command_column_ = access_column + 32; + Storage::next_command_step_ = CommandStep::ReadDestinationPixel; + break; + + case CommandStep::ReadDestinationPixel: + Storage::command_latch_ = + source[command_address(context.destination, context.arguments & 0x20) & source_mask]; + + Storage::minimum_command_column_ = access_column + 24; + Storage::next_command_step_ = CommandStep::WritePixel; + break; + + case CommandStep::WritePixel: { + const auto [mask, unmasked_colour] = command_colour_mask(context.destination); + const auto address = command_address(context.destination, context.arguments & 0x20) & destination_mask; + const uint8_t colour = unmasked_colour & mask; + context.latched_colour.reset(); + + using LogicalOperation = CommandContext::LogicalOperation; + if(!context.test_source || colour) { + switch(context.pixel_operation) { + default: + case LogicalOperation::Copy: + Storage::command_latch_ &= ~mask; + Storage::command_latch_ |= colour; + break; + case LogicalOperation::And: + Storage::command_latch_ &= ~mask | colour; + break; + case LogicalOperation::Or: + Storage::command_latch_ |= colour; + break; + case LogicalOperation::Xor: + Storage::command_latch_ ^= colour; + break; + case LogicalOperation::Not: + Storage::command_latch_ &= ~mask; + Storage::command_latch_ |= colour ^ mask; + break; + } + } + + destination[address] = Storage::command_latch_; + + Storage::command_->advance(); + Storage::update_command_step(access_column); + } break; + + case CommandStep::ReadSourceByte: { + Vector source_vector = context.source; + if(Storage::command_->y_only) { + source_vector.v[0] = context.destination.v[0]; + } + context.latched_colour.set(source[command_address(source_vector, context.arguments & 0x10) & source_mask]); + + Storage::minimum_command_column_ = access_column + 24; + Storage::next_command_step_ = CommandStep::WriteByte; + } break; + + case CommandStep::WriteByte: + destination[command_address(context.destination, context.arguments & 0x20) & destination_mask] + = context.latched_colour.has_value() ? context.latched_colour.colour : context.colour.colour; + context.latched_colour.reset(); + + Storage::command_->advance(); + Storage::update_command_step(access_column); + break; + } + } + return; } + // Copy and mutate the RAM pointer. + AddressT address = ram_pointer_; + ++ram_pointer_; + + // Determine the relevant RAM and its mask. + uint8_t *ram = ram_.data(); + AddressT mask = memory_mask(personality); + + if constexpr (is_yamaha_vdp(personality)) { + // The Yamaha increments only 14 bits of the address in TMS-compatible modes. + if(this->underlying_mode_ < ScreenMode::YamahaText80) { + ram_pointer_ = (ram_pointer_ & 0x3fff) | (address & AddressT(~0x3fff)); + } + + if(this->underlying_mode_ == ScreenMode::YamahaGraphics6 || this->underlying_mode_ == ScreenMode::YamahaGraphics7) { + // Rotate address one to the right as the hardware accesses + // the underlying banks of memory alternately but presents + // them as if linear. + address = rotate(address); + } + + // Also check whether expansion RAM is the true target here. + if(Storage::command_context_.arguments & 0x40) { + ram = Storage::expansion_ram_.data(); + mask = AddressT(Storage::expansion_ram_.size() - 1); + } + } + switch(queued_access_) { - default: return; + default: break; case MemoryAccess::Write: if constexpr (is_sega_vdp(personality)) { - if(master_system_.cram_is_selected) { + if(Storage::cram_is_selected_) { // Adjust the palette. In a Master System blue has a slightly different // scale; cf. https://www.retrorgb.com/sega-master-system-non-linear-blue-channel-findings.html constexpr uint8_t rg_scale[] = {0, 85, 170, 255}; constexpr uint8_t b_scale[] = {0, 104, 170, 255}; - master_system_.colour_ram[ram_pointer_ & 0x1f] = palette_pack( + Storage::colour_ram_[address & 0x1f] = palette_pack( rg_scale[(read_ahead_buffer_ >> 0) & 3], rg_scale[(read_ahead_buffer_ >> 2) & 3], b_scale[(read_ahead_buffer_ >> 4) & 3] @@ -343,9 +557,9 @@ template struct Base { // Schedule a CRAM dot; this is scheduled for wherever it should appear // on screen. So it's wherever the output stream would be now. Which // is output_lag cycles ago from the point of view of the input stream. - CRAMDot &dot = upcoming_cram_dots_.emplace_back(); - dot.location.column = write_pointer_.column - output_lag; - dot.location.row = write_pointer_.row; + auto &dot = Storage::upcoming_cram_dots_.emplace_back(); + dot.location.column = fetch_pointer_.column - output_lag; + dot.location.row = fetch_pointer_.row; // Handle before this row conditionally; then handle after (or, more realistically, // exactly at the end of) naturally. @@ -356,30 +570,36 @@ template struct Base { dot.location.row += dot.location.column / 342; dot.location.column %= 342; - dot.value = master_system_.colour_ram[ram_pointer_ & 0x1f]; + dot.value = Storage::colour_ram_[address & 0x1f]; break; } } - ram_[ram_pointer_ & memory_mask(personality)] = read_ahead_buffer_; + ram[address & mask] = read_ahead_buffer_; break; case MemoryAccess::Read: - read_ahead_buffer_ = ram_[ram_pointer_ & memory_mask(personality)]; + read_ahead_buffer_ = ram[address & mask]; break; } - ++ram_pointer_; queued_access_ = MemoryAccess::None; } + /// Helper for TMS dispatches; contains a switch statement with cases 0 to 170, each of the form: + /// + /// if constexpr (use_end && end == n) return; [[fallthrough]]; case n: fetcher.fetch(); + /// + /// i.e. it provides standard glue to enter a fetch sequence at any point, while the fetches themselves are templated on the cycle + /// at which they appear for neater expression. + template void dispatch(Fetcher &fetcher, int start, int end); + // Various fetchers. - template void fetch_tms_refresh(int start, int end); - template void fetch_tms_text(int start, int end); - template void fetch_tms_character(int start, int end); + template void fetch_tms_refresh(uint8_t y, int start, int end); + template void fetch_tms_text(uint8_t y, int start, int end); + template void fetch_tms_character(uint8_t y, int start, int end); - template void fetch_yamaha_refresh(int start, int end); - template void fetch_yamaha_no_sprites(int start, int end); - template void fetch_yamaha_sprites(int start, int end); + template void fetch_yamaha(uint8_t y, int start, int end); + template void fetch_yamaha(uint8_t y, int end); - template void fetch_sms(int start, int end); + template void fetch_sms(uint8_t y, int start, int end); // A helper function to output the current border colour for // the number of cycles supplied. @@ -390,15 +610,19 @@ template struct Base { bool asked_for_write_area_ = false; // Output serialisers. - void draw_tms_character(int start, int end); - void draw_tms_text(int start, int end); + template void draw_tms_character(int start, int end); + template void draw_tms_text(int start, int end); void draw_sms(int start, int end, uint32_t cram_dot); + + template void draw_yamaha(uint8_t y, int start, int end); + void draw_yamaha(uint8_t y, int start, int end); + + template void draw_sprites(uint8_t y, int start, int end, const std::array &palette, int *colour_buffer = nullptr); }; #include "Fetch.hpp" #include "Draw.hpp" -} } #endif /* TMS9918Base_hpp */ diff --git a/Components/9918/Implementation/AccessEnums.hpp b/Components/9918/Implementation/AccessEnums.hpp new file mode 100644 index 000000000..aa2328cc2 --- /dev/null +++ b/Components/9918/Implementation/AccessEnums.hpp @@ -0,0 +1,112 @@ +// +// AccessEnums.hpp +// Clock Signal +// +// Created by Thomas Harte on 26/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef AccessEnums_hpp +#define AccessEnums_hpp + + +namespace TI::TMS { + +// The screen mode is a necessary predecessor to picking the line mode, +// which is the thing latched per line. +enum class ScreenMode { + // Original TMS modes. + Blank, + Text, + MultiColour, + ColouredText, + Graphics, + + // 8-bit Sega modes. + SMSMode4, + + // New Yamaha V9938 modes. + YamahaText80, + YamahaGraphics3, + YamahaGraphics4, + YamahaGraphics5, + YamahaGraphics6, + YamahaGraphics7, + + // Rebranded Yamaha V9938 modes. + YamahaGraphics1 = ColouredText, + YamahaGraphics2 = Graphics, +}; + +constexpr int pixels_per_byte(ScreenMode mode) { + switch(mode) { + default: + case ScreenMode::Blank: return 1; + case ScreenMode::Text: return 6; + case ScreenMode::MultiColour: return 2; + case ScreenMode::ColouredText: return 8; + case ScreenMode::Graphics: return 8; + case ScreenMode::SMSMode4: return 2; + case ScreenMode::YamahaText80: return 6; + case ScreenMode::YamahaGraphics3: return 8; + case ScreenMode::YamahaGraphics4: return 2; + case ScreenMode::YamahaGraphics5: return 4; + case ScreenMode::YamahaGraphics6: return 2; + case ScreenMode::YamahaGraphics7: return 1; + } +} + +constexpr int width(ScreenMode mode) { + switch(mode) { + default: + case ScreenMode::Blank: return 0; + case ScreenMode::Text: return 240; + case ScreenMode::MultiColour: return 256; + case ScreenMode::ColouredText: return 256; + case ScreenMode::Graphics: return 256; + case ScreenMode::SMSMode4: return 256; + case ScreenMode::YamahaText80: return 480; + case ScreenMode::YamahaGraphics3: return 256; + case ScreenMode::YamahaGraphics4: return 256; + case ScreenMode::YamahaGraphics5: return 512; + case ScreenMode::YamahaGraphics6: return 512; + case ScreenMode::YamahaGraphics7: return 256; + } +} + +constexpr bool interleaves_banks(ScreenMode mode) { + return mode == ScreenMode::YamahaGraphics6 || mode == ScreenMode::YamahaGraphics7; +} + + +enum class FetchMode { + Text, + Character, + Refresh, + SMS, + Yamaha, +}; + +enum class MemoryAccess { + Read, Write, None +}; + +enum class VerticalState { + /// Describes any line on which pixels do not appear and no fetching occurs, including + /// the border, blanking and sync. + Blank, + /// A line on which pixels do not appear but fetching occurs. + Prefetch, + /// A line on which pixels appear and fetching occurs. + Pixels, +}; + +enum class SpriteMode { + Mode1, + Mode2, + MasterSystem, +}; + +} + +#endif /* AccessEnums_hpp */ diff --git a/Components/9918/Implementation/ClockConverter.hpp b/Components/9918/Implementation/ClockConverter.hpp index 641f4db8d..9e115d935 100644 --- a/Components/9918/Implementation/ClockConverter.hpp +++ b/Components/9918/Implementation/ClockConverter.hpp @@ -12,8 +12,7 @@ #include "../9918.hpp" #include "PersonalityTraits.hpp" -namespace TI { -namespace TMS { +namespace TI::TMS { enum class Clock { Internal, @@ -61,35 +60,6 @@ template struct StandardTiming { /// The number of internal cycles that must elapse between a request to read or write and /// it becoming a candidate for action. constexpr static int VRAMAccessDelay = 6; - - /// The first internal cycle at which pixels will be output in any mode other than text. - /// Pixels implicitly run from here to the end of the line. - constexpr static int FirstPixelCycle = 86 * CyclesPerLine / 342; - - /// The first internal cycle at which pixels will be output text mode. - constexpr static int FirstTextCycle = 94 * CyclesPerLine / 342; - - /// The final internal cycle at which pixels will be output text mode. - constexpr static int LastTextCycle = 334 * CyclesPerLine / 342; - - // For the below, the fixed portion of line layout is: - // - // [0, EndOfRightBorder): right border colour - // [EndOfRightBorder, StartOfSync): blank - // [StartOfSync, EndOfSync): sync - // [EndOfSync, StartOfColourBurst): blank - // [StartOfColourBurst, EndOfColourBurst): the colour burst - // [EndOfColourBurst, StartOfLeftBorder): blank - // - // The region from StartOfLeftBorder until the end is then filled with - // some combination of pixels and more border, depending on the vertical - // position of this line and the current screen mode. - constexpr static int EndOfRightBorder = 15 * CyclesPerLine / 342; - constexpr static int StartOfSync = 23 * CyclesPerLine / 342; - constexpr static int EndOfSync = 49 * CyclesPerLine / 342; - constexpr static int StartOfColourBurst = 51 * CyclesPerLine / 342; - constexpr static int EndOfColourBurst = 65 * CyclesPerLine / 342; - constexpr static int StartOfLeftBorder = 73 * CyclesPerLine / 342; }; /// Provides concrete, specific timing for the nominated personality. @@ -174,7 +144,54 @@ template class ClockConverter { int cycles_error_ = 0; }; -} + +// +// +// +template struct LineLayout; + +// Line layout is: +// +// [0, EndOfSync] sync +// (EndOfSync, StartOfColourBurst] blank +// (StartOfColourBurst, EndOfColourBurst] colour burst +// (EndOfColourBurst, EndOfLeftErase] blank +// (EndOfLeftErase, EndOfLeftBorder] border colour +// (EndOfLeftBorder, EndOfPixels] pixel content +// (EndOfPixels, EndOfRightBorder] border colour +// [EndOfRightBorder, ] blank +// +// ... with minor caveats: +// * horizontal adjust on the Yamaha VDPs is applied to EndOfLeftBorder and EndOfPixels; +// * the Sega VDPs may programatically extend the left border; and +// * text mode on all VDPs adjusts border width. + +template struct LineLayout> { + constexpr static int EndOfSync = 26; + constexpr static int StartOfColourBurst = 29; + constexpr static int EndOfColourBurst = 43; + constexpr static int EndOfLeftErase = 50; + constexpr static int EndOfLeftBorder = 63; + constexpr static int EndOfPixels = 319; + constexpr static int EndOfRightBorder = 334; + + constexpr static int TextModeEndOfLeftBorder = 69; + constexpr static int TextModeEndOfPixels = 309; +}; + +template struct LineLayout> { + constexpr static int EndOfSync = 100; + constexpr static int StartOfColourBurst = 113; + constexpr static int EndOfColourBurst = 167; + constexpr static int EndOfLeftErase = 202; + constexpr static int EndOfLeftBorder = 258; + constexpr static int EndOfPixels = 1282; + constexpr static int EndOfRightBorder = 1341; + + constexpr static int TextModeEndOfLeftBorder = 294; + constexpr static int TextModeEndOfPixels = 1254; +}; + } #endif /* ClockConverter_hpp */ diff --git a/Components/9918/Implementation/Draw.hpp b/Components/9918/Implementation/Draw.hpp index 4fe6da102..cd77919c2 100644 --- a/Components/9918/Implementation/Draw.hpp +++ b/Components/9918/Implementation/Draw.hpp @@ -9,18 +9,260 @@ #ifndef Draw_hpp #define Draw_hpp +// MARK: - Sprites, as generalised. + +template +template +void Base::draw_sprites(uint8_t y, int start, int end, const std::array &palette, int *colour_buffer) { + if(!draw_line_buffer_->sprites) { + return; + } + + auto &buffer = *draw_line_buffer_->sprites; + if(!buffer.active_sprite_slot) { + return; + } + + 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 < buffer.active_sprite_slot; ++index) { + auto &sprite = 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])); + + if constexpr (mode == SpriteMode::MasterSystem) { + // Draw all sprites into the sprite buffer. + for(int index = buffer.active_sprite_slot - 1; index >= 0; --index) { + auto &sprite = buffer.active_sprites[index]; + if(sprite.shift_position >= 16) { + continue; + } + + 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; + } + + return; + } + + if constexpr (SpriteBuffer::test_is_filling) { + assert(!buffer.is_filling); + } + + constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; + constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + const int sprite_width = sprites_16x16_ ? 16 : 8; + const int shifter_target = sprite_width << 1; + const int pixel_width = sprites_magnified_ ? sprite_width << 1 : sprite_width; + int min_sprite = 0; + + // + // Approach taken for Mode 2 sprites: + // + // (1) precompute full sprite images, at up to 32 pixels wide; + // (2) for each sprite that is marked as CC, walk backwards until the + // first sprite that is not marked CC, ORing it into the precomputed + // image at each step; + // (3) subsequently, just draw each sprite image independently. + // + if constexpr (mode == SpriteMode::Mode2) { + // Determine the lowest visible sprite; exit early if that leaves no sprites visible. + for(; min_sprite < buffer.active_sprite_slot; min_sprite++) { + auto &sprite = buffer.active_sprites[min_sprite]; + if(sprite.opaque()) { + break; + } + } + if(min_sprite == buffer.active_sprite_slot) { + return; + } + + if(!start) { + // Pre-rasterise the sprites one-by-one. + if(sprites_magnified_) { + for(int index = min_sprite; index < buffer.active_sprite_slot; index++) { + auto &sprite = buffer.active_sprites[index]; + for(int c = 0; c < 32; c+= 2) { + const int shift = (c >> 1) ^ 7; + const int bit = 1 & (sprite.image[shift >> 3] >> (shift & 7)); + + Storage::sprite_cache_[index][c] = + Storage::sprite_cache_[index][c + 1] = + (sprite.image[2] & 0xf & sprite_colour_selection_masks[bit]) | + uint8_t((bit << StatusSpriteCollisionShift) & sprite.collision_bit()); + } + } + } else { + for(int index = min_sprite; index < buffer.active_sprite_slot; index++) { + auto &sprite = buffer.active_sprites[index]; + for(int c = 0; c < 16; c++) { + const int shift = c ^ 7; + const int bit = 1 & (sprite.image[shift >> 3] >> (shift & 7)); + + Storage::sprite_cache_[index][c] = + (sprite.image[2] & 0xf & sprite_colour_selection_masks[bit]) | + uint8_t((bit << StatusSpriteCollisionShift) & sprite.collision_bit()); + } + } + } + + // Go backwards compositing any sprites that are set as OR masks onto their parents. + for(int index = buffer.active_sprite_slot - 1; index >= min_sprite + 1; --index) { + auto &sprite = buffer.active_sprites[index]; + if(sprite.opaque()) { + continue; + } + + // Sprite may affect all previous up to and cindlugin the next one that is opaque. + for(int previous_index = index - 1; previous_index >= min_sprite; --previous_index) { + // Determine region of overlap (if any). + auto &previous = buffer.active_sprites[previous_index]; + const int origin = sprite.x - previous.x; + const int x1 = std::max(0, -origin); + const int x2 = std::min(pixel_width - origin, pixel_width); + + // Composite sprites. + for(int x = x1; x < x2; x++) { + Storage::sprite_cache_[previous_index][x + origin] + |= Storage::sprite_cache_[index][x]; + } + + // If a previous opaque sprite has been found, stop. + if(previous.opaque()) { + break; + } + } + } + } + + // Draw. + for(int index = buffer.active_sprite_slot - 1; index >= min_sprite; --index) { + auto &sprite = buffer.active_sprites[index]; + const int x1 = std::max(0, start - sprite.x); + const int x2 = std::min(end - sprite.x, pixel_width); + + for(int x = x1; x < x2; x++) { + const uint8_t colour = Storage::sprite_cache_[index][x]; + + // Plot colour, if visible. + if(colour) { + pixel_origin_[sprite.x + x] = palette[colour & 0xf]; + } + + // TODO: is collision location recorded in mode 1? + + // Check for a new collision. + if(!(status_ & StatusSpriteCollision)) { + sprite_collision |= sprite_buffer[sprite.x + x]; + sprite_buffer[sprite.x + x] |= colour; + status_ |= sprite_collision & StatusSpriteCollision; + + if(status_ & StatusSpriteCollision) { + Storage::collision_location_[0] = uint16_t(x); + Storage::collision_location_[1] = uint16_t(y); + } + } + } + } + + return; + } + + if constexpr (mode == SpriteMode::Mode1) { + for(int index = buffer.active_sprite_slot - 1; index >= min_sprite; --index) { + auto &sprite = buffer.active_sprites[index]; + if(sprite.shift_position >= shifter_target) { + continue; + } + + 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] & 0xf]; + + pixel_origin_[c] = + (pixel_origin_[c] & sprite_colour_selection_masks[sprite_colour^1]) | + (palette[sprite.image[2] & 0xf] & sprite_colour_selection_masks[sprite_colour]); + + sprite.shift_position += shift_advance; + } + } + + status_ |= sprite_collision << StatusSpriteCollisionShift; + return; + } +} + +// Mode 2 logic, as I currently understand it, as a note for my future self: +// +// If a sprite is marked as 'CC' then it doesn't collide, but its colour value is +// ORd with those of all lower-numbered sprites down to the next one that is visible on +// that line and not marked CC. +// +// If no previous sprite meets that criteria, no pixels are displayed. But if one does +// then pixels are displayed even where they don't overlap with the earlier sprites. +// +// ... so in terms of my loop above, I guess I need temporary storage to accumulate +// an OR mask up until I hit a non-CC sprite, at which point I composite everything out? +// I'm not immediately sure whether I can appropriately reuse sprite_buffer, but possibly? + // MARK: - TMS9918 template +template void Base::draw_tms_character(int start, int end) { - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; + auto &line_buffer = *draw_line_buffer_; // Paint the background tiles. const int pixels_left = end - start; if(this->screen_mode_ == ScreenMode::MultiColour) { for(int c = start; c < end; ++c) { - pixel_target_[c] = palette[ - (line_buffer.patterns[c >> 3][0] >> (((c & 4)^4))) & 15 + pixel_target_[c] = palette()[ + (line_buffer.tiles.patterns[c >> 3][0] >> (((c & 4)^4))) & 15 ]; } } else { @@ -29,11 +271,11 @@ void Base::draw_tms_character(int start, int end) { int length = std::min(pixels_left, 8 - shift); - int pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]) >> shift; - uint8_t colour = line_buffer.patterns[byte_column][1]; + int pattern = Numeric::bit_reverse(line_buffer.tiles.patterns[byte_column][0]) >> shift; + uint8_t colour = line_buffer.tiles.patterns[byte_column][1]; uint32_t colours[2] = { - palette[(colour & 15) ? (colour & 15) : background_colour_], - palette[(colour >> 4) ? (colour >> 4) : background_colour_] + palette()[(colour & 15) ? (colour & 15) : background_colour_], + palette()[(colour >> 4) ? (colour >> 4) : background_colour_] }; int background_pixels_left = pixels_left; @@ -49,75 +291,42 @@ void Base::draw_tms_character(int start, int end) { length = std::min(8, background_pixels_left); byte_column++; - pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]); - colour = line_buffer.patterns[byte_column][1]; - colours[0] = palette[(colour & 15) ? (colour & 15) : background_colour_]; - colours[1] = palette[(colour >> 4) ? (colour >> 4) : background_colour_]; + pattern = Numeric::bit_reverse(line_buffer.tiles.patterns[byte_column][0]); + colour = line_buffer.tiles.patterns[byte_column][1]; + colours[0] = palette()[(colour & 15) ? (colour & 15) : background_colour_]; + colours[1] = palette()[(colour >> 4) ? (colour >> 4) : background_colour_]; } } - // Paint sprites and check for collisions, but only if at least one sprite is active - // on this line. - if(line_buffer.active_sprite_slot) { - const int shift_advance = sprites_magnified_ ? 1 : 2; - // If this is the start of the line clip any part of any sprites that is off to the left. - if(!start) { - for(int index = 0; index < line_buffer.active_sprite_slot; ++index) { - LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; - if(sprite.x < 0) sprite.shift_position -= shift_advance * sprite.x; - } - } - - int sprite_buffer[256]; - int sprite_collision = 0; - memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); - - constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; - constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; - - // Draw all sprites into the sprite buffer. - const int shifter_target = sprites_16x16_ ? 32 : 16; - for(int index = line_buffer.active_sprite_slot - 1; index >= 0; --index) { - LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; - if(sprite.shift_position < shifter_target) { - const int pixel_start = std::max(start, sprite.x); - for(int c = pixel_start; c < end && sprite.shift_position < shifter_target; ++c) { - const int shift = (sprite.shift_position >> 1) ^ 7; - int sprite_colour = (sprite.image[shift >> 3] >> (shift & 7)) & 1; - - // A colision is detected regardless of sprite colour ... - sprite_collision |= sprite_buffer[c] & sprite_colour; - sprite_buffer[c] |= sprite_colour; - - // ... but a sprite with the transparent colour won't actually be visible. - sprite_colour &= colour_masks[sprite.image[2]&15]; - pixel_origin_[c] = - (pixel_origin_[c] & sprite_colour_selection_masks[sprite_colour^1]) | - (palette[sprite.image[2]&15] & sprite_colour_selection_masks[sprite_colour]); - - sprite.shift_position += shift_advance; - } - } - } - - status_ |= sprite_collision << StatusSpriteCollisionShift; - } + draw_sprites(0, start, end, palette()); // TODO: propagate a real 'y' into here. } template +template void Base::draw_tms_text(int start, int end) { - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; - const uint32_t colours[2] = { palette[background_colour_], palette[text_colour_] }; + auto &line_buffer = *draw_line_buffer_; + uint32_t colours[2][2] = { + {palette()[background_colour_], palette()[text_colour_]}, + {0, 0} + }; + if constexpr (apply_blink) { + colours[1][0] = palette()[Storage::blink_background_colour_]; + colours[1][1] = palette()[Storage::blink_text_colour_]; + } const int shift = start % 6; int byte_column = start / 6; - int pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]) >> shift; + int pattern = Numeric::bit_reverse(line_buffer.characters.shapes[byte_column]) >> shift; int pixels_left = end - start; int length = std::min(pixels_left, 6 - shift); + int flag = 0; + if constexpr (apply_blink) { + flag = (line_buffer.characters.flags[byte_column >> 3] >> ((byte_column & 7) ^ 7)) & Storage::in_blink_; + } while(true) { pixels_left -= length; for(int c = 0; c < length; ++c) { - pixel_target_[c] = colours[pattern&0x01]; + pixel_target_[c] = colours[flag][(pattern&0x01)]; pattern >>= 1; } pixel_target_ += length; @@ -125,7 +334,10 @@ void Base::draw_tms_text(int start, int end) { if(!pixels_left) break; length = std::min(6, pixels_left); byte_column++; - pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]); + pattern = Numeric::bit_reverse(line_buffer.characters.shapes[byte_column]); + if constexpr (apply_blink) { + flag = (line_buffer.characters.flags[byte_column >> 3] >> ((byte_column & 7) ^ 7)) & Storage::in_blink_; + } } } @@ -133,158 +345,221 @@ void Base::draw_tms_text(int start, int end) { template void Base::draw_sms(int start, int end, uint32_t cram_dot) { - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; - int colour_buffer[256]; + if constexpr (is_sega_vdp(personality)) { + int colour_buffer[256]; + auto &line_buffer = *draw_line_buffer_; - /* - 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; - } + /* + Add extra border for any pixels that fall before the fine scroll. + */ + int tile_start = start, tile_end = end; + int tile_offset = start; + if(output_pointer_.row >= 16 || !Storage::horizontal_scroll_lock_) { + for(int c = start; c < (line_buffer.latched_horizontal_scroll & 7); ++c) { + colour_buffer[c] = 16 + background_colour_; + ++tile_offset; } - 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; - } + // 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); } - 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); + uint32_t pattern; + uint8_t *const pattern_index = reinterpret_cast(&pattern); - // 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); + /* + 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); - if(sprite_colour) { - sprite_collision |= sprite_buffer[c]; - sprite_buffer[c] = sprite_colour | 0x10; + pattern = *reinterpret_cast(line_buffer.tiles.patterns[byte_column]); + if(line_buffer.tiles.flags[byte_column]&2) + pattern >>= shift; + else + pattern <<= shift; + + while(true) { + const int palette_offset = (line_buffer.tiles.flags[byte_column]&0x18) << 1; + if(line_buffer.tiles.flags[byte_column]&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; } - - sprite.shift_position += shift_advance; } + + pixels_left -= length; + if(!pixels_left) break; + + length = std::min(8, pixels_left); + byte_column++; + pattern = *reinterpret_cast(line_buffer.tiles.patterns[byte_column]); } } - // 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]; + /* + Apply sprites (if any). + */ + draw_sprites(0, start, end, palette(), colour_buffer); // TODO provide good y, as per elsewhere. + + // Map from the 32-colour buffer to real output pixels, applying the specific CRAM dot if any. + pixel_target_[start] = Storage::colour_ram_[colour_buffer[start] & 0x1f] | cram_dot; + for(int c = start+1; c < end; ++c) { + pixel_target_[c] = Storage::colour_ram_[colour_buffer[c] & 0x1f]; } - if(sprite_collision) - status_ |= StatusSpriteCollision; - } - - // Map from the 32-colour buffer to real output pixels, applying the specific CRAM dot if any. - pixel_target_[start] = master_system_.colour_ram[colour_buffer[start] & 0x1f] | cram_dot; - for(int c = start+1; c < end; ++c) { - pixel_target_[c] = master_system_.colour_ram[colour_buffer[c] & 0x1f]; - } - - // If the VDP is set to hide the left column and this is the final call that'll come - // this line, hide it. - if(end == 256) { - if(master_system_.hide_left_column) { - pixel_origin_[0] = pixel_origin_[1] = pixel_origin_[2] = pixel_origin_[3] = - pixel_origin_[4] = pixel_origin_[5] = pixel_origin_[6] = pixel_origin_[7] = - master_system_.colour_ram[16 + background_colour_]; + // 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(Storage::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] = + Storage::colour_ram_[16 + background_colour_]; + } } } } // MARK: - Yamaha -// TODO. +template +template +void Base::draw_yamaha(uint8_t y, int start, int end) { + const auto active_palette = palette(); + const int sprite_start = start >> 2; + const int sprite_end = end >> 2; + auto &line_buffer = *draw_line_buffer_; + + // Observation justifying Duff's device below: it's acceptable to paint too many pixels — to paint + // beyond `end` — provided that the overpainting is within normal bitmap bounds, because any + // mispainted pixels will be replaced before becoming visible to the user. + + if constexpr (mode == ScreenMode::YamahaGraphics4 || mode == ScreenMode::YamahaGraphics6) { + start >>= (mode == ScreenMode::YamahaGraphics4) ? 2 : 1; + end >>= (mode == ScreenMode::YamahaGraphics4) ? 2 : 1; + + int column = start & ~1; + const int offset = start & 1; + start >>= 1; + end = (end + 1) >> 1; + + switch(offset) { + case 0: + do { + pixel_target_[column+0] = active_palette[line_buffer.bitmap[start] >> 4]; + case 1: pixel_target_[column+1] = active_palette[line_buffer.bitmap[start] & 0xf]; + ++start; + column += 2; + } while(start < end); + } + } + + if constexpr (mode == ScreenMode::YamahaGraphics5) { + start >>= 1; + end >>= 1; + + int column = start & ~3; + const int offset = start & 3; + start >>= 2; + end = (end + 3) >> 2; + + switch(offset) { + case 0: + do { + pixel_target_[column+0] = active_palette[line_buffer.bitmap[start] >> 6]; + case 1: pixel_target_[column+1] = active_palette[(line_buffer.bitmap[start] >> 4) & 3]; + case 2: pixel_target_[column+2] = active_palette[(line_buffer.bitmap[start] >> 2) & 3]; + case 3: pixel_target_[column+3] = active_palette[line_buffer.bitmap[start] & 3]; + ++start; + column += 4; + } while(start < end); + } + } + + if constexpr (mode == ScreenMode::YamahaGraphics7) { + start >>= 2; + end >>= 2; + + while(start < end) { + pixel_target_[start] = + palette_pack( + uint8_t((line_buffer.bitmap[start] & 0x1c) + ((line_buffer.bitmap[start] & 0x1c) << 3) + ((line_buffer.bitmap[start] & 0x1c) >> 3)), + uint8_t((line_buffer.bitmap[start] & 0xe0) + ((line_buffer.bitmap[start] & 0xe0) >> 3) + ((line_buffer.bitmap[start] & 0xe0) >> 6)), + uint8_t((line_buffer.bitmap[start] & 0x03) + ((line_buffer.bitmap[start] & 0x03) << 2) + ((line_buffer.bitmap[start] & 0x03) << 4) + ((line_buffer.bitmap[start] & 0x03) << 6)) + ); + ++start; + } + } + + constexpr std::array graphics7_sprite_palette = { + palette_pack(0b00000000, 0b00000000, 0b00000000), palette_pack(0b00000000, 0b00000000, 0b01001001), + palette_pack(0b00000000, 0b01101101, 0b00000000), palette_pack(0b00000000, 0b01101101, 0b01001001), + palette_pack(0b01101101, 0b00000000, 0b00000000), palette_pack(0b01101101, 0b00000000, 0b01001001), + palette_pack(0b01101101, 0b01101101, 0b00000000), palette_pack(0b01101101, 0b01101101, 0b01001001), + + palette_pack(0b10010010, 0b11111111, 0b01001001), palette_pack(0b00000000, 0b00000000, 0b11111111), + palette_pack(0b00000000, 0b11111111, 0b00000000), palette_pack(0b00000000, 0b11111111, 0b11111111), + palette_pack(0b11111111, 0b00000000, 0b00000000), palette_pack(0b11111111, 0b00000000, 0b11111111), + palette_pack(0b11111111, 0b11111111, 0b00000000), palette_pack(0b11111111, 0b11111111, 0b11111111), + }; + + // Possibly TODO: is the data-sheet trying to allege some sort of colour mixing for sprites in Mode 6? + draw_sprites< + SpriteMode::Mode2, + mode == ScreenMode::YamahaGraphics5 || mode == ScreenMode::YamahaGraphics6 + >(y, sprite_start, sprite_end, mode == ScreenMode::YamahaGraphics7 ? graphics7_sprite_palette : palette()); +} + +template +void Base::draw_yamaha(uint8_t y, int start, int end) { + if constexpr (is_yamaha_vdp(personality)) { + switch(draw_line_buffer_->screen_mode) { + // Modes that are the same (or close enough) to those on the TMS. + case ScreenMode::Text: draw_tms_text(start >> 2, end >> 2); break; + case ScreenMode::YamahaText80: draw_tms_text(start >> 1, end >> 1); break; + case ScreenMode::MultiColour: + case ScreenMode::ColouredText: + case ScreenMode::Graphics: draw_tms_character(start >> 2, end >> 2); break; + + case ScreenMode::YamahaGraphics3: + draw_tms_character(start >> 2, end >> 2); + break; + +#define Dispatch(x) case ScreenMode::x: draw_yamaha(y, start, end); break; + Dispatch(YamahaGraphics4); + Dispatch(YamahaGraphics5); + Dispatch(YamahaGraphics6); + Dispatch(YamahaGraphics7); +#undef Dispatch + + default: break; + } + } +} // MARK: - Mega Drive diff --git a/Components/9918/Implementation/Fetch.hpp b/Components/9918/Implementation/Fetch.hpp index a2367de93..d4644c328 100644 --- a/Components/9918/Implementation/Fetch.hpp +++ b/Components/9918/Implementation/Fetch.hpp @@ -16,7 +16,15 @@ 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 + 3) within each sequencer, time 0 is the access window that straddles the beginning of + horizontal sync. Which, conveniently, is the place to which Grauw's timing diagrams + are aligned. + 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. + + [Historically: + position 0 was 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; @@ -26,9 +34,8 @@ 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. + I'm moving away from this per the desire not to have V9938 output straddle two lines if horizontally-adjusted, + amongst other concerns.] Provided for the benefit of the methods below: @@ -41,457 +48,758 @@ for the exceptions. */ -#define slot(n) \ - if(use_end && end == n) return; \ - [[fallthrough]]; \ - case n +// MARK: - Address mask helpers. -#define external_slot(n) \ - slot(n): do_external_slot(to_internal(n)); +/// @returns An instance of @c AddressT with all top bits set down to and including +/// bit @c end and all others clear. +/// +/// So e.g. if @c AddressT is @c uint16_t and this VDP has a 15-bit address space then +/// @c top_bits<10> will be the address with bits 15 to 10 (inclusive) set and the rest clear. +template constexpr AddressT top_bits() { + return AddressT(~0) - AddressT((1 << end) - 1); +} -#define external_slots_2(n) \ - external_slot(n); \ - external_slot(n+1); +/// Modifies and returns @c source so that all bits above position @c n are set; the others are unmodified. +template constexpr AddressT bits(AddressT source = 0) { + return AddressT(source | top_bits()); +} -#define external_slots_4(n) \ - external_slots_2(n); \ - external_slots_2(n+2); - -#define external_slots_8(n) \ - external_slots_4(n); \ - external_slots_4(n+4); - -#define external_slots_16(n) \ - external_slots_8(n); \ - external_slots_8(n+8); - -#define external_slots_32(n) \ - external_slots_16(n); \ - external_slots_16(n+16); - - -// MARK: - TMS9918 +// MARK: - 171-window Dispatcher. template -template void Base::fetch_tms_refresh(int start, int end) { -#define refresh(location) \ - slot(location): \ - external_slot(location+1); - -#define refreshes_2(location) \ - refresh(location); \ - refresh(location+2); - -#define refreshes_4(location) \ - refreshes_2(location); \ - refreshes_2(location+4); - -#define refreshes_8(location) \ - refreshes_4(location); \ - refreshes_4(location+8); +template void Base::dispatch(SequencerT &fetcher, int start, int end) { +#define index(n) \ + if(use_end && end == n) return; \ + [[fallthrough]]; \ + case n: fetcher.template fetch(n); 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; + index(0); index(1); index(2); index(3); index(4); index(5); index(6); index(7); index(8); index(9); + index(10); index(11); index(12); index(13); index(14); index(15); index(16); index(17); index(18); index(19); + index(20); index(21); index(22); index(23); index(24); index(25); index(26); index(27); index(28); index(29); + index(30); index(31); index(32); index(33); index(34); index(35); index(36); index(37); index(38); index(39); + index(40); index(41); index(42); index(43); index(44); index(45); index(46); index(47); index(48); index(49); + index(50); index(51); index(52); index(53); index(54); index(55); index(56); index(57); index(58); index(59); + index(60); index(61); index(62); index(63); index(64); index(65); index(66); index(67); index(68); index(69); + index(70); index(71); index(72); index(73); index(74); index(75); index(76); index(77); index(78); index(79); + index(80); index(81); index(82); index(83); index(84); index(85); index(86); index(87); index(88); index(89); + index(90); index(91); index(92); index(93); index(94); index(95); index(96); index(97); index(98); index(99); + index(100); index(101); index(102); index(103); index(104); index(105); index(106); index(107); index(108); index(109); + index(110); index(111); index(112); index(113); index(114); index(115); index(116); index(117); index(118); index(119); + index(120); index(121); index(122); index(123); index(124); index(125); index(126); index(127); index(128); index(129); + index(130); index(131); index(132); index(133); index(134); index(135); index(136); index(137); index(138); index(139); + index(140); index(141); index(142); index(143); index(144); index(145); index(146); index(147); index(148); index(149); + index(150); index(151); index(152); index(153); index(154); index(155); index(156); index(157); index(158); index(159); + index(160); index(161); index(162); index(163); index(164); index(165); index(166); index(167); index(168); index(169); + index(170); } -#undef refreshes_8 -#undef refreshes_4 -#undef refreshes_2 -#undef refresh +#undef index +} + +// MARK: - Fetchers. + +template +struct TextFetcher { + using AddressT = typename Base::AddressT; + + TextFetcher(Base *base, uint8_t y) : + base(base), + row_base(base->pattern_name_address_ & bits<10>(AddressT((y >> 3) * 40))), + row_offset(base->pattern_generator_table_address_ & bits<11>(AddressT(y & 7))) {} + + void fetch_name(AddressT column, int slot = 0) { + base->name_[slot] = base->ram_[row_base + column]; + } + + void fetch_pattern(AddressT column, int slot = 0) { + base->fetch_line_buffer_->characters.shapes[column] = base->ram_[row_offset + size_t(base->name_[slot] << 3)]; + } + + Base *const base; + const AddressT row_base; + const AddressT row_offset; +}; + +template +struct CharacterFetcher { + using AddressT = typename Base::AddressT; + + CharacterFetcher(Base *base, uint8_t y) : + base(base), + y(y), + row_base(base->pattern_name_address_ & bits<10>(AddressT((y << 2)&~31))) + { + pattern_base = base->pattern_generator_table_address_; + colour_base = base->colour_table_address_; + colour_name_shift = 6; + + const ScreenMode mode = base->fetch_line_buffer_->screen_mode; + if(mode == ScreenMode::Graphics || mode == ScreenMode::YamahaGraphics3) { + // If this is high resolution mode, allow the row number to affect the pattern and colour addresses. + pattern_base &= bits<13>(AddressT(((y & 0xc0) << 5))); + colour_base &= bits<13>(AddressT(((y & 0xc0) << 5))); + + colour_base += AddressT(y & 7); + colour_name_shift = 0; + } else { + colour_base &= bits<6, AddressT>(); + pattern_base &= bits<11, AddressT>(); + } + + if(mode == ScreenMode::MultiColour) { + pattern_base += AddressT((y >> 2) & 7); + } else { + pattern_base += AddressT(y & 7); + } + } + + void fetch_name(int column) { + base->tile_offset_ = base->ram_[row_base + AddressT(column)]; + } + + void fetch_pattern(int column) { + base->fetch_line_buffer_->tiles.patterns[column][0] = base->ram_[pattern_base + AddressT(base->tile_offset_ << 3)]; + } + + void fetch_colour(int column) { + base->fetch_line_buffer_->tiles.patterns[column][1] = base->ram_[colour_base + AddressT((base->tile_offset_ << 3) >> colour_name_shift)]; + } + + Base *const base; + const uint8_t y; + const AddressT row_base; + AddressT pattern_base; + AddressT colour_base; + int colour_name_shift; +}; + +constexpr SpriteMode sprite_mode(ScreenMode screen_mode) { + switch(screen_mode) { + default: + return SpriteMode::Mode2; + + case ScreenMode::MultiColour: + case ScreenMode::ColouredText: + case ScreenMode::Graphics: + return SpriteMode::Mode1; + + case ScreenMode::SMSMode4: + return SpriteMode::MasterSystem; + } +} + +// TODO: should this be extended to include Master System sprites? +template +class SpriteFetcher { + public: + using AddressT = typename Base::AddressT; + + // The Yamaha VDP adds an additional table when in Sprite Mode 2, the sprite colour + // table, which is intended to fill the 512 bytes before the programmer-located sprite + // attribute table. + // + // It partially enforces this proximity by forcing bits 7 and 8 to 0 in the address of + // the attribute table, and forcing them to 1 but masking out bit 9 for the colour table. + // + // AttributeAddressMask is used to enable or disable that behaviour. + static constexpr AddressT AttributeAddressMask = (mode == SpriteMode::Mode2) ? AddressT(~0x180) : AddressT(~0x000); + + SpriteFetcher(Base *base, uint8_t y) : + base(base), + y(y) {} + + void fetch_location(int slot) { + fetch_xy(slot); + + if constexpr (mode == SpriteMode::Mode2) { + fetch_xy(slot + 1); + + base->name_[0] = name(slot); + base->name_[1] = name(slot + 1); + } + } + + void fetch_pattern(int slot) { + switch(mode) { + case SpriteMode::Mode1: + fetch_image(slot, name(slot)); + break; + + case SpriteMode::Mode2: + fetch_image(slot, base->name_[0]); + fetch_image(slot + 1, base->name_[1]); + break; + } + } + + void fetch_y(int sprite) { + const AddressT address = base->sprite_attribute_table_address_ & AttributeAddressMask & bits<7>(AddressT(sprite << 2)); + const uint8_t sprite_y = base->ram_[address]; + base->posit_sprite(sprite, sprite_y, y); + } + + private: + void fetch_xy(int slot) { + auto &buffer = *base->fetch_sprite_buffer_; + buffer.active_sprites[slot].x = + base->ram_[ + base->sprite_attribute_table_address_ & AttributeAddressMask & bits<7>(AddressT((buffer.active_sprites[slot].index << 2) | 1)) + ]; + } + + uint8_t name(int slot) { + auto &buffer = *base->fetch_sprite_buffer_; + const AddressT address = + base->sprite_attribute_table_address_ & + AttributeAddressMask & + bits<7>(AddressT((buffer.active_sprites[slot].index << 2) | 2)); + const uint8_t name = base->ram_[address] & (base->sprites_16x16_ ? ~3 : ~0); + return name; + } + + void fetch_image(int slot, uint8_t name) { + uint8_t colour = 0; + auto &sprite = base->fetch_sprite_buffer_->active_sprites[slot]; + switch(mode) { + case SpriteMode::Mode1: + // Fetch colour from the attribute table, per this sprite's slot. + colour = base->ram_[ + base->sprite_attribute_table_address_ & bits<7>(AddressT((sprite.index << 2) | 3)) + ]; + break; + + case SpriteMode::Mode2: { + // Fetch colour from the colour table, per this sprite's slot and row. + const AddressT colour_table_address = (base->sprite_attribute_table_address_ | ~AttributeAddressMask) & AddressT(~0x200); + colour = base->ram_[ + colour_table_address & + bits<9>( + AddressT(sprite.index << 4) | + AddressT(sprite.row) + ) + ]; + } break; + } + sprite.image[2] = colour; + sprite.x -= sprite.early_clock(); + + const AddressT graphic_location = base->sprite_generator_table_address_ & bits<11>(AddressT((name << 3) | sprite.row)); + sprite.image[0] = base->ram_[graphic_location]; + sprite.image[1] = base->ram_[graphic_location+16]; + + if constexpr (SpriteBuffer::test_is_filling) { + if(slot == ((mode == SpriteMode::Mode2) ? 7 : 3)) { + base->fetch_sprite_buffer_->is_filling = false; + } + } + } + + Base *const base; + const uint8_t y; +}; + +template +struct SMSFetcher { + using AddressT = typename Base::AddressT; + struct RowInfo { + AddressT pattern_address_base; + AddressT sub_row[2]; + }; + + SMSFetcher(Base *base, uint8_t y) : + base(base), + storage(static_cast *>(base)), + y(y), + horizontal_offset((y >= 16 || !storage->horizontal_scroll_lock_) ? (base->fetch_line_buffer_->latched_horizontal_scroll >> 3) : 0) + { + // Limit address bits in use if this is a SMS2 mode. + const bool is_tall_mode = base->mode_timing_.pixel_lines != 192; + const AddressT pattern_name_address = storage->pattern_name_address_ | (is_tall_mode ? 0x800 : 0); + const AddressT pattern_name_offset = is_tall_mode ? 0x100 : 0; + + // Determine row info for the screen both (i) if vertical scrolling is applied; and (ii) if it isn't. + // The programmer can opt out of applying vertical scrolling to the right-hand portion of the display. + const int scrolled_row = (y + storage->latched_vertical_scroll_) % (is_tall_mode ? 256 : 224); + scrolled_row_info.pattern_address_base = (pattern_name_address & bits<11>(AddressT((scrolled_row & ~7) << 3))) - pattern_name_offset; + scrolled_row_info.sub_row[0] = AddressT((scrolled_row & 7) << 2); + scrolled_row_info.sub_row[1] = AddressT(28 ^ ((scrolled_row & 7) << 2)); + if(storage->vertical_scroll_lock_) { + static_row_info.pattern_address_base = bits<11>(AddressT(pattern_name_address & ((y & ~7) << 3))) - pattern_name_offset; + static_row_info.sub_row[0] = AddressT((y & 7) << 2); + static_row_info.sub_row[1] = 28 ^ AddressT((y & 7) << 2); + } else static_row_info = scrolled_row_info; + } + + void fetch_sprite(int sprite) { + auto &sprite_buffer = *base->fetch_sprite_buffer_; + sprite_buffer.active_sprites[sprite].x = + base->ram_[ + storage->sprite_attribute_table_address_ & bits<7>((sprite_buffer.active_sprites[sprite].index << 1) | 0) + ] - (storage->shift_sprites_8px_left_ ? 8 : 0); + const uint8_t name = base->ram_[ + storage->sprite_attribute_table_address_ & bits<7>((sprite_buffer.active_sprites[sprite].index << 1) | 1) + ] & (base->sprites_16x16_ ? ~1 : ~0); + + const AddressT graphic_location = + storage->sprite_generator_table_address_ & + bits<13>(AddressT((name << 5) | (sprite_buffer.active_sprites[sprite].row << 2))); + sprite_buffer.active_sprites[sprite].image[0] = base->ram_[graphic_location]; + sprite_buffer.active_sprites[sprite].image[1] = base->ram_[graphic_location+1]; + sprite_buffer.active_sprites[sprite].image[2] = base->ram_[graphic_location+2]; + sprite_buffer.active_sprites[sprite].image[3] = base->ram_[graphic_location+3]; + } + + void fetch_tile_name(int column) { + const RowInfo &row_info = column < 24 ? scrolled_row_info : static_row_info; + const size_t scrolled_column = (column - horizontal_offset) & 0x1f; + const size_t address = row_info.pattern_address_base + (scrolled_column << 1); + auto &line_buffer = *base->fetch_line_buffer_; + + line_buffer.tiles.flags[column] = base->ram_[address+1]; + base->tile_offset_ = AddressT( + (((line_buffer.tiles.flags[column]&1) << 8) | base->ram_[address]) << 5 + ) + row_info.sub_row[(line_buffer.tiles.flags[column]&4) >> 2]; + } + + void fetch_tile_pattern(int column) { + auto &line_buffer = *base->fetch_line_buffer_; + line_buffer.tiles.patterns[column][0] = base->ram_[base->tile_offset_]; + line_buffer.tiles.patterns[column][1] = base->ram_[base->tile_offset_+1]; + line_buffer.tiles.patterns[column][2] = base->ram_[base->tile_offset_+2]; + line_buffer.tiles.patterns[column][3] = base->ram_[base->tile_offset_+3]; + } + + void posit_sprite(int sprite) { + base->posit_sprite(sprite, base->ram_[storage->sprite_attribute_table_address_ & bits<8>(AddressT(sprite))], y); + } + + Base *const base; + const Storage *const storage; + const uint8_t y; + const int horizontal_offset; + RowInfo scrolled_row_info, static_row_info; +}; + +// MARK: - TMS Sequencers. + +template +struct RefreshSequencer { + RefreshSequencer(Base *base) : base(base) {} + + template void fetch(int c) { + if(cycle < 26 || (cycle & 1) || cycle >= 154) { + base->do_external_slot(to_internal(c)); + } + } + + Base *const base; +}; + +template +struct TextSequencer { + template TextSequencer(Args&&... args) : fetcher(std::forward(args)...) {} + + template void fetch(int c) { + // The first 30 and the final 4 slots are external. + if constexpr (cycle < 30 || cycle >= 150) { + fetcher.base->do_external_slot(to_internal(c)); + return; + } else { + // For the 120 slots in between follow a three-step pattern of: + constexpr int offset = cycle - 30; + constexpr auto column = AddressT(offset / 3); + switch(offset % 3) { + case 0: fetcher.fetch_name(column); break; // (1) fetch tile name. + case 1: fetcher.base->do_external_slot(to_internal(c)); break; // (2) external slot. + case 2: fetcher.fetch_pattern(column); break; // (3) fetch tile pattern. + } + } + } + + using AddressT = typename Base::AddressT; + TextFetcher fetcher; +}; + +template +struct CharacterSequencer { + template CharacterSequencer(Args&&... args) : + character_fetcher(std::forward(args)...), + sprite_fetcher(std::forward(args)...) {} + + template void fetch(int c) { + if(cycle < 5) { + character_fetcher.base->do_external_slot(to_internal(c)); + } + + if(cycle == 5) { + // Fetch: n1, c2, pat2a, pat2b, y3, x3, n3, c3, pat3a, pat3b. + sprite_fetcher.fetch_pattern(2); + sprite_fetcher.fetch_location(3); + sprite_fetcher.fetch_pattern(3); + } + + if(cycle > 14 && cycle < 19) { + character_fetcher.base->do_external_slot(to_internal(c)); + } + + // Fetch 8 new sprite Y coordinates, to begin selecting sprites for next line. + if(cycle == 19) { + sprite_fetcher.fetch_y(0); sprite_fetcher.fetch_y(1); sprite_fetcher.fetch_y(2); sprite_fetcher.fetch_y(3); + sprite_fetcher.fetch_y(4); sprite_fetcher.fetch_y(5); sprite_fetcher.fetch_y(6); sprite_fetcher.fetch_y(7); + } + + // Body of line: tiles themselves, plus some additional potential sprites. + if(cycle >= 27 && cycle < 155) { + constexpr int offset = cycle - 27; + constexpr int block = offset >> 2; + constexpr int sub_block = offset & 3; + switch(sub_block) { + case 0: character_fetcher.fetch_name(block); break; + case 1: + if(!(block & 3)) { + character_fetcher.base->do_external_slot(to_internal(c)); + } else { + constexpr int sprite = 8 + ((block >> 2) * 3) + ((block & 3) - 1); + sprite_fetcher.fetch_y(sprite); + } + break; + case 2: + character_fetcher.fetch_pattern(block); + character_fetcher.fetch_colour(block); + break; + default: break; + } + } + + if(cycle >= 155 && cycle < 157) { + character_fetcher.base->do_external_slot(to_internal(c)); + } + + if(cycle == 157) { + // Fetch: y0, x0, n0, c0, pat0a, pat0b, y1, x1, n1, c1, pat1a, pat1b, y2, x2. + sprite_fetcher.fetch_location(0); + sprite_fetcher.fetch_pattern(0); + sprite_fetcher.fetch_location(1); + sprite_fetcher.fetch_pattern(1); + sprite_fetcher.fetch_location(2); + } + } + + using AddressT = typename Base::AddressT; + CharacterFetcher character_fetcher; + SpriteFetcher sprite_fetcher; +}; + +// MARK: - TMS fetch routines. + +template +template void Base::fetch_tms_refresh(uint8_t, int start, int end) { + RefreshSequencer sequencer(this); + dispatch(sequencer, start, end); } template -template void Base::fetch_tms_text(int start, int end) { -#define fetch_tile_name(location, column) slot(location): line_buffer.names[column].offset = ram_[row_base + column]; -#define fetch_tile_pattern(location, column) slot(location): line_buffer.patterns[column][0] = ram_[row_offset + size_t(line_buffer.names[column].offset << 3)]; - -#define fetch_column(location, column) \ - fetch_tile_name(location, column); \ - external_slot(location+1); \ - fetch_tile_pattern(location+2, column); - -#define fetch_columns_2(location, column) \ - fetch_column(location, column); \ - fetch_column(location+3, column+1); - -#define fetch_columns_4(location, column) \ - fetch_columns_2(location, column); \ - fetch_columns_2(location+6, column+2); - -#define fetch_columns_8(location, column) \ - fetch_columns_4(location, column); \ - fetch_columns_4(location+12, column+4); - - LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; - const size_t row_base = pattern_name_address_ & (0x3c00 | size_t(write_pointer_.row >> 3) * 40); - const size_t row_offset = pattern_generator_table_address_ & (0x3800 | (write_pointer_.row & 7)); - - switch(start) { - default: assert(false); - - /* 47 external slots (= 47 windows) */ - external_slots_32(0) - external_slots_8(32) - external_slots_4(40) - external_slots_2(44) - external_slot(46) - - /* 40 column fetches (= 120 windows) */ - fetch_columns_8(47, 0); - fetch_columns_8(71, 8); - fetch_columns_8(95, 16); - fetch_columns_8(119, 24); - fetch_columns_8(143, 32); - - /* 5 more external slots */ - external_slots_4(167); - external_slot(171); - - return; - } - -#undef fetch_columns_8 -#undef fetch_columns_4 -#undef fetch_columns_2 -#undef fetch_column -#undef fetch_tile_pattern -#undef fetch_tile_name +template void Base::fetch_tms_text(uint8_t y, int start, int end) { + TextSequencer sequencer(this, y); + dispatch(sequencer, start, end); } template -template void Base::fetch_tms_character(int start, int end) { -#define sprite_fetch_coordinates(location, sprite) \ - slot(location): \ - slot(location+1): \ - line_buffer.active_sprites[sprite].x = \ - ram_[\ - sprite_attribute_table_address_ & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 2))\ - ]; - - // This implementation doesn't refetch Y; it's unclear to me - // whether it's refetched. - -#define sprite_fetch_graphics(location, sprite) \ - slot(location): \ - slot(location+1): \ - slot(location+2): \ - slot(location+3): {\ - const uint8_t name = ram_[\ - sprite_attribute_table_address_ & size_t(0x3f82 | (line_buffer.active_sprites[sprite].index << 2))\ - ] & (sprites_16x16_ ? ~3 : ~0);\ - line_buffer.active_sprites[sprite].image[2] = ram_[\ - sprite_attribute_table_address_ & size_t(0x3f83 | (line_buffer.active_sprites[sprite].index << 2))\ - ];\ - line_buffer.active_sprites[sprite].x -= (line_buffer.active_sprites[sprite].image[2] & 0x80) >> 2;\ - const size_t graphic_location = sprite_generator_table_address_ & size_t(0x3800 | (name << 3) | line_buffer.active_sprites[sprite].row); \ - line_buffer.active_sprites[sprite].image[0] = ram_[graphic_location];\ - line_buffer.active_sprites[sprite].image[1] = ram_[graphic_location+16];\ - } - -#define sprite_fetch_block(location, sprite) \ - sprite_fetch_coordinates(location, sprite) \ - sprite_fetch_graphics(location+2, sprite) - -#define sprite_y_read(location, sprite) \ - slot(location): posit_sprite(sprite_selection_buffer, sprite, ram_[sprite_attribute_table_address_ & (((sprite) << 2) | 0x3f80)], write_pointer_.row); - -#define fetch_tile_name(column) line_buffer.names[column].offset = ram_[(row_base + column) & 0x3fff]; - -#define fetch_tile(column) {\ - line_buffer.patterns[column][1] = ram_[(colour_base + size_t((line_buffer.names[column].offset << 3) >> colour_name_shift)) & 0x3fff]; \ - line_buffer.patterns[column][0] = ram_[(pattern_base + size_t(line_buffer.names[column].offset << 3)) & 0x3fff]; \ - } - -#define background_fetch_block(location, column, sprite) \ - slot(location): fetch_tile_name(column) \ - external_slot(location+1); \ - slot(location+2): \ - slot(location+3): fetch_tile(column) \ - slot(location+4): fetch_tile_name(column+1) \ - sprite_y_read(location+5, sprite); \ - slot(location+6): \ - slot(location+7): fetch_tile(column+1) \ - slot(location+8): fetch_tile_name(column+2) \ - sprite_y_read(location+9, sprite+1); \ - slot(location+10): \ - slot(location+11): fetch_tile(column+2) \ - slot(location+12): fetch_tile_name(column+3) \ - sprite_y_read(location+13, sprite+2); \ - slot(location+14): \ - slot(location+15): fetch_tile(column+3) - - LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; - LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines]; - const size_t row_base = pattern_name_address_ & (size_t((write_pointer_.row << 2)&~31) | 0x3c00); - - size_t pattern_base = pattern_generator_table_address_; - size_t colour_base = colour_table_address_; - int colour_name_shift = 6; - - if(screen_mode_ == ScreenMode::Graphics) { - // If this is high resolution mode, allow the row number to affect the pattern and colour addresses. - pattern_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5)); - colour_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5)); - - colour_base += size_t(write_pointer_.row & 7); - colour_name_shift = 0; - } else { - colour_base &= size_t(0xffc0); - pattern_base &= size_t(0x3800); - } - - if(screen_mode_ == ScreenMode::MultiColour) { - pattern_base += size_t((write_pointer_.row >> 2) & 7); - } else { - pattern_base += size_t(write_pointer_.row & 7); - } - - switch(start) { - default: assert(false); - - external_slots_2(0); - - sprite_fetch_block(2, 0); - sprite_fetch_block(8, 1); - sprite_fetch_coordinates(14, 2); - - external_slots_4(16); - external_slot(20); - - sprite_fetch_graphics(21, 2); - sprite_fetch_block(25, 3); - - slot(31): - sprite_selection_buffer.reset_sprite_collection(); - do_external_slot(to_internal(31)); - external_slots_2(32); - external_slot(34); - - sprite_y_read(35, 0); - sprite_y_read(36, 1); - sprite_y_read(37, 2); - sprite_y_read(38, 3); - sprite_y_read(39, 4); - sprite_y_read(40, 5); - sprite_y_read(41, 6); - sprite_y_read(42, 7); - - background_fetch_block(43, 0, 8); - background_fetch_block(59, 4, 11); - background_fetch_block(75, 8, 14); - background_fetch_block(91, 12, 17); - background_fetch_block(107, 16, 20); - background_fetch_block(123, 20, 23); - background_fetch_block(139, 24, 26); - background_fetch_block(155, 28, 29); - - return; - } - -#undef background_fetch_block -#undef fetch_tile -#undef fetch_tile_name -#undef sprite_y_read -#undef sprite_fetch_block -#undef sprite_fetch_graphics -#undef sprite_fetch_coordinates +template void Base::fetch_tms_character(uint8_t y, int start, int end) { + CharacterSequencer sequencer(this, y); + dispatch(sequencer, start, end); } - // MARK: - Master System template -template void Base::fetch_sms(int start, int end) { -#define sprite_fetch(sprite) {\ - line_buffer.active_sprites[sprite].x = \ - ram_[\ - master_system_.sprite_attribute_table_address & size_t(0x3f80 | (line_buffer.active_sprites[sprite].index << 1))\ - ] - (master_system_.shift_sprites_8px_left ? 8 : 0); \ - const uint8_t name = ram_[\ - master_system_.sprite_attribute_table_address & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 1))\ - ] & (sprites_16x16_ ? ~1 : ~0);\ - const size_t graphic_location = master_system_.sprite_generator_table_address & size_t(0x2000 | (name << 5) | (line_buffer.active_sprites[sprite].row << 2)); \ - line_buffer.active_sprites[sprite].image[0] = ram_[graphic_location]; \ - line_buffer.active_sprites[sprite].image[1] = ram_[graphic_location+1]; \ - line_buffer.active_sprites[sprite].image[2] = ram_[graphic_location+2]; \ - line_buffer.active_sprites[sprite].image[3] = ram_[graphic_location+3]; \ +struct SMSSequencer { + template SMSSequencer(Args&&... args) : fetcher(std::forward(args)...) {} + + // Cf. https://www.smspower.org/forums/16485-GenesisMode4VRAMTiming with this implementation pegging + // window 0 to HSYNC low. + template void fetch(int c) { + if(cycle < 3) { + fetcher.base->do_external_slot(to_internal(c)); + } + + if(cycle == 3) { + fetcher.fetch_sprite(4); + fetcher.fetch_sprite(5); + fetcher.fetch_sprite(6); + fetcher.fetch_sprite(7); + } + + if(cycle == 15 || cycle == 16) { + fetcher.base->do_external_slot(to_internal(c)); + } + + if(cycle == 17) { + fetcher.posit_sprite(0); fetcher.posit_sprite(1); fetcher.posit_sprite(2); fetcher.posit_sprite(3); + fetcher.posit_sprite(4); fetcher.posit_sprite(5); fetcher.posit_sprite(6); fetcher.posit_sprite(7); + fetcher.posit_sprite(8); fetcher.posit_sprite(9); fetcher.posit_sprite(10); fetcher.posit_sprite(11); + fetcher.posit_sprite(12); fetcher.posit_sprite(13); fetcher.posit_sprite(14); fetcher.posit_sprite(15); + } + + if(cycle >= 25 && cycle < 153) { + constexpr int offset = cycle - 39; + constexpr int block = offset >> 2; + constexpr int sub_block = offset & 3; + + switch(sub_block) { + default: break; + + case 0: fetcher.fetch_tile_name(block); break; + case 1: + if(!(block & 3)) { + fetcher.base->do_external_slot(to_internal(c)); + } else { + constexpr int sprite = (8 + ((block >> 2) * 3) + ((block & 3) - 1)) << 1; + fetcher.posit_sprite(sprite); + fetcher.posit_sprite(sprite+1); + } + break; + case 2: fetcher.fetch_tile_pattern(block); break; + } + } + + if(cycle >= 153 && cycle < 157) { + fetcher.base->do_external_slot(to_internal(c)); + } + + if(cycle == 157) { + fetcher.fetch_sprite(0); + fetcher.fetch_sprite(1); + fetcher.fetch_sprite(2); + fetcher.fetch_sprite(3); + } + + if(cycle >= 169) { + fetcher.base->do_external_slot(to_internal(c)); + } } -#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); + using AddressT = typename Base::AddressT; + SMSFetcher fetcher; +}; -#define sprite_y_read(location, sprite) \ - slot(location): \ - posit_sprite(sprite_selection_buffer, sprite, ram_[master_system_.sprite_attribute_table_address & ((sprite) | 0x3f00)], write_pointer_.row); \ - posit_sprite(sprite_selection_buffer, sprite+1, ram_[master_system_.sprite_attribute_table_address & ((sprite + 1) | 0x3f00)], write_pointer_.row); \ - -#define fetch_tile_name(column, row_info) {\ - const size_t scrolled_column = (column - horizontal_offset) & 0x1f;\ - const size_t address = row_info.pattern_address_base + (scrolled_column << 1); \ - line_buffer.names[column].flags = ram_[address+1]; \ - line_buffer.names[column].offset = size_t( \ - (((line_buffer.names[column].flags&1) << 8) | ram_[address]) << 5 \ - ) + row_info.sub_row[(line_buffer.names[column].flags&4) >> 2]; \ +template +template void Base::fetch_sms(uint8_t y, int start, int end) { + if constexpr (is_sega_vdp(personality)) { + SMSSequencer sequencer(this, y); + dispatch(sequencer, start, end); } - -#define fetch_tile(column) \ - line_buffer.patterns[column][0] = ram_[line_buffer.names[column].offset]; \ - line_buffer.patterns[column][1] = ram_[line_buffer.names[column].offset+1]; \ - line_buffer.patterns[column][2] = ram_[line_buffer.names[column].offset+2]; \ - line_buffer.patterns[column][3] = ram_[line_buffer.names[column].offset+3]; - -#define background_fetch_block(location, column, sprite, row_info) \ - slot(location): fetch_tile_name(column, row_info) \ - external_slot(location+1); \ - slot(location+2): \ - slot(location+3): \ - slot(location+4): \ - fetch_tile(column) \ - fetch_tile_name(column+1, row_info) \ - sprite_y_read(location+5, sprite); \ - slot(location+6): \ - slot(location+7): \ - slot(location+8): \ - fetch_tile(column+1) \ - fetch_tile_name(column+2, row_info) \ - sprite_y_read(location+9, sprite+2); \ - slot(location+10): \ - slot(location+11): \ - slot(location+12): \ - fetch_tile(column+2) \ - fetch_tile_name(column+3, row_info) \ - sprite_y_read(location+13, sprite+4); \ - slot(location+14): \ - slot(location+15): fetch_tile(column+3) - - // Determine the coarse horizontal scrolling offset; this isn't applied on the first two lines if the programmer has requested it. - LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; - LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines]; - const int horizontal_offset = (write_pointer_.row >= 16 || !master_system_.horizontal_scroll_lock) ? (line_buffer.latched_horizontal_scroll >> 3) : 0; - - // Limit address bits in use if this is a SMS2 mode. - const bool is_tall_mode = mode_timing_.pixel_lines != 192; - const size_t pattern_name_address = master_system_.pattern_name_address | (is_tall_mode ? 0x800 : 0); - const size_t pattern_name_offset = is_tall_mode ? 0x100 : 0; - - // Determine row info for the screen both (i) if vertical scrolling is applied; and (ii) if it isn't. - // The programmer can opt out of applying vertical scrolling to the right-hand portion of the display. - const int scrolled_row = (write_pointer_.row + master_system_.latched_vertical_scroll) % (is_tall_mode ? 256 : 224); - struct RowInfo { - size_t pattern_address_base; - size_t sub_row[2]; - }; - const RowInfo scrolled_row_info = { - (pattern_name_address & size_t(((scrolled_row & ~7) << 3) | 0x3800)) - pattern_name_offset, - {size_t((scrolled_row & 7) << 2), 28 ^ size_t((scrolled_row & 7) << 2)} - }; - RowInfo row_info; - if(master_system_.vertical_scroll_lock) { - row_info.pattern_address_base = (pattern_name_address & size_t(((write_pointer_.row & ~7) << 3) | 0x3800)) - pattern_name_offset; - row_info.sub_row[0] = size_t((write_pointer_.row & 7) << 2); - row_info.sub_row[1] = 28 ^ size_t((write_pointer_.row & 7) << 2); - } else row_info = scrolled_row_info; - - // ... and do the actual fetching, which follows this routine: - switch(start) { - default: assert(false); - - sprite_fetch_block(0, 0); - sprite_fetch_block(6, 2); - - external_slots_4(12); - external_slot(16); - - sprite_fetch_block(17, 4); - sprite_fetch_block(23, 6); - - slot(29): - sprite_selection_buffer.reset_sprite_collection(); - do_external_slot(to_internal(29)); - external_slot(30); - - sprite_y_read(31, 0); - sprite_y_read(32, 2); - sprite_y_read(33, 4); - sprite_y_read(34, 6); - sprite_y_read(35, 8); - sprite_y_read(36, 10); - sprite_y_read(37, 12); - sprite_y_read(38, 14); - - background_fetch_block(39, 0, 16, scrolled_row_info); - background_fetch_block(55, 4, 22, scrolled_row_info); - background_fetch_block(71, 8, 28, scrolled_row_info); - background_fetch_block(87, 12, 34, scrolled_row_info); - background_fetch_block(103, 16, 40, scrolled_row_info); - background_fetch_block(119, 20, 46, scrolled_row_info); - background_fetch_block(135, 24, 52, row_info); - background_fetch_block(151, 28, 58, row_info); - - external_slots_4(167); - - return; - } - -#undef background_fetch_block -#undef fetch_tile -#undef fetch_tile_name -#undef sprite_y_read -#undef sprite_fetch_block -#undef sprite_fetch } // MARK: - Yamaha -// TODO. - template -template void Base::fetch_yamaha_refresh(int start, int end) { - (void)start; - (void)end; +template void Base::fetch_yamaha(uint8_t y, int end) { + CharacterFetcher character_fetcher(this, y); + TextFetcher text_fetcher(this, y); + SpriteFetcher sprite_fetcher(this, y); + + using Type = typename Storage::Event::Type; + while(Storage::next_event_->offset < end) { + switch(Storage::next_event_->type) { + case Type::External: + do_external_slot(Storage::next_event_->offset); + break; + + case Type::Name: + switch(mode) { + case ScreenMode::Text: { + const auto column = AddressT(Storage::next_event_->id << 1); + + text_fetcher.fetch_name(column, 0); + text_fetcher.fetch_name(column + 1, 1); + } break; + + case ScreenMode::YamahaText80: { + const auto column = AddressT(Storage::next_event_->id << 2); + const auto start = pattern_name_address_ & bits<12>(AddressT((y >> 3) * 80)); + + name_[0] = ram_[start + column + 0]; + name_[1] = ram_[start + column + 1]; + name_[2] = ram_[start + column + 2]; + name_[3] = ram_[start + column + 3]; + } break; + + case ScreenMode::Graphics: + case ScreenMode::MultiColour: + case ScreenMode::ColouredText: + character_fetcher.fetch_name(Storage::next_event_->id); + break; + + default: break; + } + break; + + case Type::Colour: + switch(mode) { + case ScreenMode::YamahaText80: { + const auto column = AddressT(Storage::next_event_->id); + const auto address = colour_table_address_ & bits<9>(AddressT((y >> 3) * 10)); + auto &line_buffer = *fetch_line_buffer_; + line_buffer.characters.flags[column] = ram_[address + column]; + } break; + + case ScreenMode::Graphics: + case ScreenMode::MultiColour: + case ScreenMode::ColouredText: + character_fetcher.fetch_colour(Storage::next_event_->id); + break; + + default: break; + } + break; + + case Type::Pattern: + switch(mode) { + case ScreenMode::Text: { + const auto column = AddressT(Storage::next_event_->id << 1); + + text_fetcher.fetch_pattern(column, 0); + text_fetcher.fetch_pattern(column + 1, 1); + } break; + + case ScreenMode::YamahaText80: { + const auto column = Storage::next_event_->id << 2; + const auto start = pattern_generator_table_address_ & bits<11>(AddressT(y & 7)); + auto &line_buffer = *fetch_line_buffer_; + + line_buffer.characters.shapes[column + 0] = ram_[start + AddressT(name_[0] << 3)]; + line_buffer.characters.shapes[column + 1] = ram_[start + AddressT(name_[1] << 3)]; + line_buffer.characters.shapes[column + 2] = ram_[start + AddressT(name_[2] << 3)]; + line_buffer.characters.shapes[column + 3] = ram_[start + AddressT(name_[3] << 3)]; + } break; + + case ScreenMode::Graphics: + case ScreenMode::MultiColour: + case ScreenMode::ColouredText: + character_fetcher.fetch_pattern(Storage::next_event_->id); + break; + + case ScreenMode::YamahaGraphics3: + // As per comment elsewhere; my _guess_ is that G3 is slotted as if it were + // a bitmap mode, with the three bytes that describe each column fitting into + // the relevant windows. + character_fetcher.fetch_name(Storage::next_event_->id); + character_fetcher.fetch_colour(Storage::next_event_->id); + character_fetcher.fetch_pattern(Storage::next_event_->id); + break; + + case ScreenMode::YamahaGraphics4: + case ScreenMode::YamahaGraphics5: { + const int column = Storage::next_event_->id << 2; + const auto start = bits<15>((y << 7) | column); + auto &line_buffer = *fetch_line_buffer_; + + line_buffer.bitmap[column + 0] = ram_[pattern_name_address_ & AddressT(start + 0)]; + line_buffer.bitmap[column + 1] = ram_[pattern_name_address_ & AddressT(start + 1)]; + line_buffer.bitmap[column + 2] = ram_[pattern_name_address_ & AddressT(start + 2)]; + line_buffer.bitmap[column + 3] = ram_[pattern_name_address_ & AddressT(start + 3)]; + } break; + + case ScreenMode::YamahaGraphics6: + case ScreenMode::YamahaGraphics7: { + const uint8_t *const ram2 = &ram_[65536]; + const int column = Storage::next_event_->id << 3; + const auto start = bits<15>((y << 7) | (column >> 1)); + auto &line_buffer = *fetch_line_buffer_; + + // Fetch from alternate banks. + line_buffer.bitmap[column + 0] = ram_[pattern_name_address_ & AddressT(start + 0) & 0xffff]; + line_buffer.bitmap[column + 1] = ram2[pattern_name_address_ & AddressT(start + 0) & 0xffff]; + line_buffer.bitmap[column + 2] = ram_[pattern_name_address_ & AddressT(start + 1) & 0xffff]; + line_buffer.bitmap[column + 3] = ram2[pattern_name_address_ & AddressT(start + 1) & 0xffff]; + line_buffer.bitmap[column + 4] = ram_[pattern_name_address_ & AddressT(start + 2) & 0xffff]; + line_buffer.bitmap[column + 5] = ram2[pattern_name_address_ & AddressT(start + 2) & 0xffff]; + line_buffer.bitmap[column + 6] = ram_[pattern_name_address_ & AddressT(start + 3) & 0xffff]; + line_buffer.bitmap[column + 7] = ram2[pattern_name_address_ & AddressT(start + 3) & 0xffff]; + } break; + + default: break; + } + break; + + case Type::SpriteY: + switch(mode) { + case ScreenMode::Blank: + case ScreenMode::Text: + case ScreenMode::YamahaText80: + // Ensure the compiler can discard character_fetcher in these modes. + break; + + default: + sprite_fetcher.fetch_y(Storage::next_event_->id); + break; + } + break; + + case Type::SpriteLocation: + switch(mode) { + case ScreenMode::Blank: + case ScreenMode::Text: + case ScreenMode::YamahaText80: + // Ensure the compiler can discard character_fetcher in these modes. + break; + + default: + sprite_fetcher.fetch_location(Storage::next_event_->id); + break; + } + break; + + case Type::SpritePattern: + switch(mode) { + case ScreenMode::Blank: + case ScreenMode::Text: + case ScreenMode::YamahaText80: + // Ensure the compiler can discard character_fetcher in these modes. + break; + + default: + sprite_fetcher.fetch_pattern(Storage::next_event_->id); + break; + } + break; + + default: break; + } + + ++Storage::next_event_; + } } -template -template void Base::fetch_yamaha_no_sprites(int start, int end) { - (void)start; - (void)end; -} template -template void Base::fetch_yamaha_sprites(int start, int end) { - (void)start; - (void)end; +template void Base::fetch_yamaha(uint8_t y, int, int end) { + if constexpr (is_yamaha_vdp(personality)) { + // Dispatch according to [supported] screen mode. +#define Dispatch(mode) case mode: fetch_yamaha(y, end); break; + switch(fetch_line_buffer_->screen_mode) { + default: break; + Dispatch(ScreenMode::Blank); + Dispatch(ScreenMode::Text); + Dispatch(ScreenMode::MultiColour); + Dispatch(ScreenMode::ColouredText); + Dispatch(ScreenMode::Graphics); + Dispatch(ScreenMode::YamahaText80); + Dispatch(ScreenMode::YamahaGraphics3); + Dispatch(ScreenMode::YamahaGraphics4); + Dispatch(ScreenMode::YamahaGraphics5); + Dispatch(ScreenMode::YamahaGraphics6); + Dispatch(ScreenMode::YamahaGraphics7); + } +#undef Dispatch + } } // MARK: - Mega Drive // TODO. -#undef external_slot -#undef slot - #endif /* Fetch_hpp */ diff --git a/Components/9918/Implementation/LineBuffer.hpp b/Components/9918/Implementation/LineBuffer.hpp new file mode 100644 index 000000000..0c1e80f96 --- /dev/null +++ b/Components/9918/Implementation/LineBuffer.hpp @@ -0,0 +1,132 @@ +// +// LineBuffer.hpp +// Clock Signal +// +// Created by Thomas Harte on 12/02/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef LineBuffer_hpp +#define LineBuffer_hpp + +#include "AccessEnums.hpp" + +namespace TI::TMS { + +// Temporary buffers collect a representation of each line prior to pixel serialisation. + +struct SpriteBuffer { + // An active sprite is one that has been selected for composition onto + // _this_ line. + struct ActiveSprite { + int index = 0; // The original in-table index of this sprite. + int row = 0; // The row of the sprite that should be drawn. + int x = 0; // The sprite's x position on screen. + + uint8_t image[4]; // Up to four bytes of image information. + // + // In practice: + // + // Master System mode: the four bytes of this 8x8 sprite; + // TMS and Yamaha: [0] = the left half of this sprite; [1] = the right side (if 16x16 sprites enabled); [2] = colour, early-clock bit, etc. + int shift_position = 0; // An offset representing how much of the image information has already been drawn. + + // Yamaha helpers. + bool opaque() const { + return !(image[2] & 0x40); + } + + /// @returns @c 0x20 if this sprite should generate collisions; @c 0x00 otherwise. + int collision_bit() const { + return ((image[2] & 0x20) | ((image[2] & 0x40) >> 1)) ^ 0x20; + } + + // Yamaha and TMS helpers. + int early_clock() const { + return (image[2] & 0x80) >> 2; + } + } active_sprites[8]; + + int active_sprite_slot = 0; // A pointer to the slot into which a new active sprite will be deposited, if required. + bool sprites_stopped = false; // A special TMS feature is that a sentinel value can be used to prevent any further sprites + // being evaluated for display. This flag determines whether the sentinel has yet been reached. + uint8_t sprite_terminator = 0; + +#ifndef NDEBUG + static constexpr bool test_is_filling = true; +#else + static constexpr bool test_is_filling = false; +#endif + bool is_filling = false; + + void reset_sprite_collection(); +}; + +struct LineBuffer { + LineBuffer() {} + + // The fetch mode describes the proper timing diagram for this line; + // screen mode captures proper output mode. + FetchMode fetch_mode = FetchMode::Text; + ScreenMode screen_mode = ScreenMode::Text; + VerticalState vertical_state = VerticalState::Blank; + SpriteBuffer *sprites = nullptr; + + // 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. + union { + // This struct captures maximal potential detail across the TMS9918 + // and Sega VDP for tiled modes (plus multicolour). + struct { + uint8_t flags[32]{}; + + // 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[32][4]{}; + } tiles; + + // The Yamaha and TMS both have text modes, with the former going up to + // 80 columns plus 10 bytes of colour-esque flags. + struct { + uint8_t shapes[80]; + uint8_t flags[10]; + } characters; + + // The Yamaha VDP also has a variety of bitmap modes, + // the widest of which is 512px @ 4bpp. + uint8_t bitmap[256]; + }; + + /* + 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; + int pixel_count = 256; +}; + +struct LineBufferPointer { + int row = 0, column = 0; +}; + +} + +#endif /* LineBuffer_hpp */ diff --git a/Components/9918/Implementation/PersonalityTraits.hpp b/Components/9918/Implementation/PersonalityTraits.hpp index 985a4d02a..7a4fd013c 100644 --- a/Components/9918/Implementation/PersonalityTraits.hpp +++ b/Components/9918/Implementation/PersonalityTraits.hpp @@ -9,8 +9,7 @@ #ifndef PersonalityTraits_hpp #define PersonalityTraits_hpp -namespace TI { -namespace TMS { +namespace TI::TMS { // Genus determinants for the various personalityes. constexpr bool is_sega_vdp(Personality p) { @@ -42,12 +41,10 @@ constexpr size_t memory_size(Personality p) { } } -constexpr uint16_t memory_mask(Personality p) { - return (memory_size(p) >= 65536) ? 0xffff : uint16_t(memory_size(p) - 1); +constexpr size_t memory_mask(Personality p) { + return memory_size(p) - 1; } } -} - #endif /* PersonalityTraits_hpp */ diff --git a/Components/9918/Implementation/Storage.hpp b/Components/9918/Implementation/Storage.hpp new file mode 100644 index 000000000..53382e29e --- /dev/null +++ b/Components/9918/Implementation/Storage.hpp @@ -0,0 +1,489 @@ +// +// +// Storage.hpp +// Clock Signal +// +// Created by Thomas Harte on 12/02/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef Storage_h +#define Storage_h + +#include "LineBuffer.hpp" +#include "YamahaCommands.hpp" + +#include + +namespace TI::TMS { + +/// A container for personality-specific storage; see specific instances below. +template struct Storage { +}; + +template <> struct Storage { + using AddressT = uint16_t; + + void begin_line(ScreenMode, bool) {} +}; + +// Yamaha-specific storage. +template struct Storage> { + using AddressT = uint32_t; + + std::array expansion_ram_; + + int selected_status_ = 0; + + int indirect_register_ = 0; + bool increment_indirect_register_ = false; + + int adjustment_[2]{}; + + std::array palette_{}; + std::array background_palette_{}; + bool solid_background_ = true; + + uint8_t new_colour_ = 0; + uint8_t palette_entry_ = 0; + bool palette_write_phase_ = false; + + uint8_t mode_ = 0; + + uint8_t vertical_offset_ = 0; + uint8_t sprite_cache_[8][32]{}; + + /// Describes an _observable_ memory access event. i.e. anything that it is safe + /// (and convenient) to treat as atomic in between external slots. + struct Event { + /// Offset of the _beginning_ of the event. Not completely arbitrarily: this is when + /// external data must be ready by in order to take part in those slots. + uint16_t offset = 1368; + enum class Type: uint8_t { + /// A slot for reading or writing data on behalf of the CPU or the command engine. + External, + + // + // Sprites. + // + SpriteY, + SpriteLocation, + SpritePattern, + + // + // Backgrounds. + // + Name, + Colour, + Pattern, + } type = Type::External; + uint8_t id = 0; + + constexpr Event(Type type, uint8_t id = 0) noexcept : + type(type), + id(id) {} + + constexpr Event() noexcept {} + }; + + // State that tracks fetching position within a line. + const Event *next_event_ = nullptr; + + // Text blink colours. + uint8_t blink_text_colour_ = 0; + uint8_t blink_background_colour_ = 0; + + // Blink state (which is also affects even/odd page display in applicable modes). + int in_blink_ = 1; + uint8_t blink_periods_ = 0; + uint8_t blink_counter_ = 0; + + // Sprite collection state. + bool sprites_enabled_ = true; + + // Additional status. + uint8_t colour_status_ = 0; + uint16_t colour_location_ = 0; + uint16_t collision_location_[2]{}; + + /// Resets line-ephemeral state for a new line. + void begin_line(ScreenMode mode, bool is_refresh) { + if(is_refresh) { + next_event_ = refresh_events.data(); + return; + } + + switch(mode) { + case ScreenMode::YamahaText80: + case ScreenMode::Text: + next_event_ = text_events.data(); + break; + + case ScreenMode::MultiColour: + case ScreenMode::YamahaGraphics1: + case ScreenMode::YamahaGraphics2: + next_event_ = character_events.data(); + break; + + case ScreenMode::YamahaGraphics3: // TODO: verify; my guess is that G3 is timed like a bitmap mode + // in order to fit the pattern for sprite mode 2. Just a guess. + default: + next_event_ = sprites_enabled_ ? sprites_events.data() : no_sprites_events.data(); + break; + } + } + + // Command engine state. + CommandContext command_context_; + ModeDescription mode_description_; + std::unique_ptr command_ = nullptr; + + enum class CommandStep { + None, + + CopySourcePixelToStatus, + + ReadSourcePixel, + ReadDestinationPixel, + WritePixel, + + ReadSourceByte, + WriteByte, + }; + CommandStep next_command_step_ = CommandStep::None; + int minimum_command_column_ = 0; + uint8_t command_latch_ = 0; + + void update_command_step(int current_column) { + if(!command_) { + next_command_step_ = CommandStep::None; + return; + } + if(command_->done()) { + command_ = nullptr; + next_command_step_ = CommandStep::None; + return; + } + + minimum_command_column_ = current_column + command_->cycles; + switch(command_->access) { + case Command::AccessType::ReadPoint: + next_command_step_ = CommandStep::CopySourcePixelToStatus; + break; + + case Command::AccessType::CopyPoint: + next_command_step_ = CommandStep::ReadSourcePixel; + break; + case Command::AccessType::PlotPoint: + next_command_step_ = CommandStep::ReadDestinationPixel; + break; + + case Command::AccessType::WaitForColourReceipt: + // i.e. nothing to do until a colour is received. + next_command_step_ = CommandStep::None; + break; + + case Command::AccessType::CopyByte: + next_command_step_ = CommandStep::ReadSourceByte; + break; + case Command::AccessType::WriteByte: + next_command_step_ = CommandStep::WriteByte; + break; + } + } + + Storage() noexcept { + // Perform sanity checks on the event lists. +#ifndef NDEBUG + const Event *lists[] = { no_sprites_events.data(), sprites_events.data(), text_events.data(), character_events.data(), refresh_events.data(), nullptr }; + const Event **list = lists; + while(*list) { + const Event *cursor = *list; + ++list; + + while(cursor[1].offset != 1368) { + assert(cursor[1].offset > cursor[0].offset); + ++cursor; + } + } +#endif + + // Seed to _something_ meaningful. + // + // TODO: this is a workaround [/hack], in effect, for the main TMS' habit of starting + // in a randomised position, which means that start-of-line isn't announced. + // + // Do I really want that behaviour? + next_event_ = refresh_events.data(); + } + + private: + template static constexpr size_t events_size() { + size_t size = 0; + for(int c = 0; c < 1368; c++) { + const auto event_type = GeneratorT::event(c); + size += event_type.has_value(); + } + return size + 1; + } + + template ()> + static constexpr std::array events() { + std::array result{}; + size_t index = 0; + for(int c = 0; c < 1368; c++) { + const auto event = GeneratorT::event(c); + if(!event) { + continue; + } + result[index] = *event; + result[index].offset = uint16_t(c); + ++index; + } + result[index] = Event(); + return result; + } + + struct StandardGenerators { + static constexpr std::optional external_every_eight(int index) { + if(index & 7) return std::nullopt; + return Event::Type::External; + } + }; + + struct RefreshGenerator { + static constexpr std::optional event(int grauw_index) { + // From 0 to 126: CPU/CMD slots at every cycle divisible by 8. + if(grauw_index < 126) { + return StandardGenerators::external_every_eight(grauw_index - 0); + } + + // From 164 to 1234: eight-cycle windows, the first 15 of each 16 being + // CPU/CMD and the final being refresh. + if(grauw_index >= 164 && grauw_index < 1234) { + const int offset = grauw_index - 164; + if(offset & 7) return std::nullopt; + if(((offset >> 3) & 15) == 15) return std::nullopt; + return Event::Type::External; + } + + // From 1268 to 1330: CPU/CMD slots at every cycle divisible by 8. + if(grauw_index >= 1268 && grauw_index < 1330) { + return StandardGenerators::external_every_eight(grauw_index - 1268); + } + + // A CPU/CMD at 1334. + if(grauw_index == 1334) { + return Event::Type::External; + } + + // From 1344 to 1366: CPU/CMD slots every cycle divisible by 8. + if(grauw_index >= 1344 && grauw_index < 1366) { + return StandardGenerators::external_every_eight(grauw_index - 1344); + } + + // Otherwise: nothing. + return std::nullopt; + } + }; + static constexpr auto refresh_events = events(); + + template struct BitmapGenerator { + static constexpr std::optional event(int grauw_index) { + if(!include_sprites) { + // Various standard zones of one-every-eight external slots. + if(grauw_index < 124) { + return StandardGenerators::external_every_eight(grauw_index + 2); + } + if(grauw_index > 1266) { + return StandardGenerators::external_every_eight(grauw_index - 1266); + } + } else { + // This records collection points for all data for selected sprites. + // There's only four of them (each site covering two sprites), + // so it's clearer just to be explicit. + // + // There's also a corresponding number of extra external slots to spell out. + switch(grauw_index) { + default: break; + case 1238: return Event(Event::Type::SpriteLocation, 0); + case 1302: return Event(Event::Type::SpriteLocation, 2); + case 2: return Event(Event::Type::SpriteLocation, 4); + case 66: return Event(Event::Type::SpriteLocation, 6); + case 1270: return Event(Event::Type::SpritePattern, 0); + case 1338: return Event(Event::Type::SpritePattern, 2); + case 34: return Event(Event::Type::SpritePattern, 4); + case 98: return Event(Event::Type::SpritePattern, 6); + case 1264: case 1330: case 28: case 92: + return Event::Type::External; + } + } + + if(grauw_index >= 162 && grauw_index < 176) { + return StandardGenerators::external_every_eight(grauw_index - 162); + } + + // Everywhere else the pattern is: + // + // external or sprite y, external, data block + // + // Subject to caveats: + // + // 1) the first data block is just a dummy fetch with no side effects, + // so this emulator declines to record it; and + // 2) every fourth block, the second external is actually a refresh. + // + if(grauw_index >= 182 && grauw_index < 1238) { + const int offset = grauw_index - 182; + const int block = offset / 32; + const int sub_block = offset & 31; + + switch(sub_block) { + default: return std::nullopt; + case 0: + if(include_sprites) { + // Don't include the sprite post-amble (i.e. a spurious read with no side effects). + if(block < 32) { + return Event(Event::Type::SpriteY, uint8_t(block)); + } + } else { + return Event::Type::External; + } + case 6: + if((block & 3) != 3) { + return Event::Type::External; + } + break; + case 12: + if(block) { + return Event(Event::Type::Pattern, uint8_t(block - 1)); + } + break; + } + } + + return std::nullopt; + } + }; + static constexpr auto no_sprites_events = events>(); + static constexpr auto sprites_events = events>(); + + struct TextGenerator { + static constexpr std::optional event(int grauw_index) { + // Capture various one-in-eight zones. + if(grauw_index < 72) { + return StandardGenerators::external_every_eight(grauw_index - 2); + } + if(grauw_index >= 166 && grauw_index < 228) { + return StandardGenerators::external_every_eight(grauw_index - 166); + } + if(grauw_index >= 1206 && grauw_index < 1332) { + return StandardGenerators::external_every_eight(grauw_index - 1206); + } + if(grauw_index == 1336) { + return Event::Type::External; + } + if(grauw_index >= 1346) { + return StandardGenerators::external_every_eight(grauw_index - 1346); + } + + // Elsewhere... + if(grauw_index >= 246) { + const int offset = grauw_index - 246; + const int block = offset / 48; + const int sub_block = offset % 48; + switch(sub_block) { + default: break; + case 0: return Event(Event::Type::Name, uint8_t(block)); + case 18: return (block & 1) ? Event::Type::External : Event(Event::Type::Colour, uint8_t(block >> 1)); + case 24: return Event(Event::Type::Pattern, uint8_t(block)); + } + } + + return std::nullopt; + } + }; + static constexpr auto text_events = events(); + + struct CharacterGenerator { + static constexpr std::optional event(int grauw_index) { + // Grab sprite events. + switch(grauw_index) { + default: break; + case 1242: return Event(Event::Type::SpriteLocation, 0); + case 1306: return Event(Event::Type::SpriteLocation, 1); + case 6: return Event(Event::Type::SpriteLocation, 2); + case 70: return Event(Event::Type::SpriteLocation, 3); + case 1274: return Event(Event::Type::SpritePattern, 0); + case 1342: return Event(Event::Type::SpritePattern, 1); + case 38: return Event(Event::Type::SpritePattern, 2); + case 102: return Event(Event::Type::SpritePattern, 3); + case 1268: case 1334: case 32: case 96: return Event::Type::External; + } + + if(grauw_index >= 166 && grauw_index < 180) { + return StandardGenerators::external_every_eight(grauw_index - 166); + } + + if(grauw_index >= 182 && grauw_index < 1238) { + const int offset = grauw_index - 182; + const int block = offset / 32; + const int sub_block = offset & 31; + switch(sub_block) { + case 0: if(block > 0) return Event(Event::Type::Name, uint8_t(block - 1)); + case 6: if((sub_block & 3) != 3) return Event::Type::External; + case 12: if(block < 32) return Event(Event::Type::SpriteY, uint8_t(block)); + case 18: if(block > 0) return Event(Event::Type::Pattern, uint8_t(block - 1)); + case 24: if(block > 0) return Event(Event::Type::Colour, uint8_t(block - 1)); + } + } + + return std::nullopt; + } + }; + static constexpr auto character_events = events(); +}; + +// Master System-specific storage. +template struct Storage> { + using AddressT = uint16_t; + + // The SMS VDP has a programmer-set colour palette, with a dedicated patch of RAM. But the RAM is only exactly + // fast enough for the pixel clock. So when the programmer writes to it, that causes a one-pixel glitch; there + // isn't the bandwidth for the read both write to occur simultaneously. The following buffer therefore keeps + // track of pending collisions, for visual reproduction. + struct CRAMDot { + LineBufferPointer location; + uint32_t value; + }; + std::vector upcoming_cram_dots_; + + // The Master System's additional colour RAM. + uint32_t colour_ram_[32]; + bool cram_is_selected_ = false; + + // 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; + + // 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; + + // Various resource addresses with VDP-version-specific modifications + // built in. + AddressT pattern_name_address_; + AddressT sprite_attribute_table_address_; + AddressT sprite_generator_table_address_; + + void begin_line(ScreenMode, bool) {} +}; + +} + +#endif /* Storage_h */ diff --git a/Components/9918/Implementation/YamahaCommands.hpp b/Components/9918/Implementation/YamahaCommands.hpp new file mode 100644 index 000000000..6254950bf --- /dev/null +++ b/Components/9918/Implementation/YamahaCommands.hpp @@ -0,0 +1,382 @@ +// +// YamahaCommands.hpp +// Clock Signal +// +// Created by Thomas Harte on 26/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef YamahaCommands_hpp +#define YamahaCommands_hpp + +#include "AccessEnums.hpp" + +namespace TI::TMS { + +// MARK: - Generics. + +struct Vector { + int v[2]{}; + + template void set(uint8_t value) { + constexpr uint8_t mask = high ? (offset ? 0x3 : 0x1) : 0xff; + constexpr int shift = high ? 8 : 0; + v[offset] = (v[offset] & ~(mask << shift)) | ((value & mask) << shift); + } + + template void add(int amount) { + v[offset] += amount; + + if constexpr (offset == 1) { + v[offset] &= 0x3ff; + } else { + v[offset] &= 0x1ff; + } + } + + Vector & operator += (const Vector &rhs) { + add<0>(rhs.v[0]); + add<1>(rhs.v[1]); + return *this; + } +}; + +struct Colour { + void set(uint8_t value) { + colour = value; + colour4bpp = uint8_t((value & 0xf) | (value << 4)); + colour2bpp = uint8_t((colour4bpp & 0x33) | ((colour4bpp & 0x33) << 2)); + } + + void reset() { + colour = 0x00; + colour4bpp = 0xff; + } + + bool has_value() const { + return (colour & 0xf) == (colour4bpp & 0xf); + } + + /// Colour as written by the CPU. + uint8_t colour = 0x00; + /// The low four bits of the CPU-written colour, repeated twice. + uint8_t colour4bpp = 0xff; + /// The low two bits of the CPU-written colour, repeated four times. + uint8_t colour2bpp = 0xff; +}; + +struct CommandContext { + Vector source; + Vector destination; + Vector size; + + uint8_t arguments = 0; + Colour colour; + Colour latched_colour; + + enum class LogicalOperation { + Copy = 0b0000, + And = 0b0001, + Or = 0b0010, + Xor = 0b0011, + Not = 0b0100, + }; + LogicalOperation pixel_operation; + bool test_source; +}; + +struct ModeDescription { + int width = 256; + int pixels_per_byte = 4; + bool rotate_address = false; +}; + +struct Command { + // In net: + // + // This command is blocked until @c access has been performed, reading + // from or writing to @c value. It should not be performed until at least + // @c cycles have passed. + enum class AccessType { + /// Plots a single pixel of the current contextual colour at @c destination, + /// which occurs as a read, then a 24-cycle gap, then a write. + PlotPoint, + + /// Blocks until the next CPU write to the colour register. + WaitForColourReceipt, + + /// Writes an entire byte to the address containing the current @c destination. + WriteByte, + + /// Copies a single pixel from @c source location to @c destination, + /// being a read, a 32-cycle gap, then a PlotPoint. + CopyPoint, + + /// Copies a complete byte from @c source location to @c destination, + /// being a read, a 24-cycle gap, then a write. + CopyByte, + + /// Copies a single pixel from @c source to the colour status register. + ReadPoint, + +// ReadByte, +// WaitForColourSend, + }; + AccessType access = AccessType::PlotPoint; + int cycles = 0; + bool is_cpu_transfer = false; + bool y_only = false; + + /// Current command parameters. + CommandContext &context; + ModeDescription &mode_description; + Command(CommandContext &context, ModeDescription &mode_description) : context(context), mode_description(mode_description) {} + virtual ~Command() {} + + /// @returns @c true if all output from this command is done; @c false otherwise. + virtual bool done() = 0; + + /// Repopulates the fields above with the next action to take, being provided with the + /// number of pixels per byte in the current screen mode. + virtual void advance() = 0; + + protected: + template void advance_axis(int offset = 1) { + context.destination.add(context.arguments & (0x4 << axis) ? -offset : offset); + if constexpr (include_source) { + context.source.add(context.arguments & (0x4 << axis) ? -offset : offset); + } + } +}; + +namespace Commands { + +// MARK: - Line drawing. + +/// Implements the LINE command, which is plain-old Bresenham. +/// +/// Per Grauw timing is: +/// +/// * 88 cycles between every pixel plot; +/// * plus an additional 32 cycles if a step along the minor axis is taken. +struct Line: public Command { + public: + Line(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) { + // context.destination = start position; + // context.size.v[0] = long side dots; + // context.size.v[1] = short side dots; + // context.arguments => direction + + position_ = context.size.v[1]; + numerator_ = position_ << 1; + denominator_ = context.size.v[0] << 1; + + cycles = 32; + access = AccessType::PlotPoint; + } + + bool done() final { + return !context.size.v[0]; + } + + void advance() final { + --context.size.v[0]; + cycles = 88; + + // b0: 1 => long direction is y; + // 0 => long direction is x. + // + // b2: 1 => x direction is left; + // 0 => x direction is right. + // + // b3: 1 => y direction is up; + // 0 => y direction is down. + if(context.arguments & 0x1) { + advance_axis<1, false>(); + } else { + advance_axis<0, false>(); + } + + position_ -= numerator_; + if(position_ < 0) { + position_ += denominator_; + cycles += 32; + + if(context.arguments & 0x1) { + advance_axis<0, false>(); + } else { + advance_axis<1, false>(); + } + } + } + + private: + int position_, numerator_, denominator_, duration_; +}; + +// MARK: - Single pixel manipulation. + +/// Implements the PSET command, which plots a single pixel and POINT, which reads one. +/// +/// No timings are documented, so this'll output or input as quickly as possible. +template struct Point: public Command { + public: + Point(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) { + cycles = 0; // TODO. + access = is_read ? AccessType::ReadPoint : AccessType::PlotPoint; + } + + bool done() final { + return done_; + } + + void advance() final { + done_ = true; + } + + private: + bool done_ = false; +}; + +// MARK: - Rectangular base. + +/// Useful base class for anything that does logical work in a rectangle. +template struct Rectangle: public Command { + public: + Rectangle(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) { + if constexpr (include_source) { + start_x_[0] = context.source.v[0]; + } + start_x_[1] = context.destination.v[0]; + width_ = context.size.v[0]; + + if(!width_) { + // Width = 0 => maximal width for this mode. + // (aside: it's still unclear to me whether commands are + // automatically clipped to the display; I think so but + // don't want to spend any time on it until I'm certain) +// context.size.v[0] = width_ = mode_description.width; + } + } + + /// Advances the current destination and, if @c include_source is @c true also the source; + /// @returns @c true if a new row was started; @c false otherwise. + bool advance_pixel() { + if constexpr (logical) { + advance_axis<0, include_source>(); + --context.size.v[0]; + + if(context.size.v[0]) { + return false; + } + } else { + advance_axis<0, include_source>(mode_description.pixels_per_byte); + context.size.v[0] -= mode_description.pixels_per_byte; + + if(context.size.v[0] & ~(mode_description.pixels_per_byte - 1)) { + return false; + } + } + + context.size.v[0] = width_; + if constexpr (include_source) { + context.source.v[0] = start_x_[0]; + } + context.destination.v[0] = start_x_[1]; + + advance_axis<1, include_source>(); + --context.size.v[1]; + + return true; + } + + bool done() final { + return !context.size.v[1] || !width_; + } + + private: + int start_x_[2]{}, width_ = 0; +}; + +// MARK: - Rectangular moves to/from CPU. + +template struct MoveFromCPU: public Rectangle { + MoveFromCPU(CommandContext &context, ModeDescription &mode_description) : Rectangle(context, mode_description) { + Command::is_cpu_transfer = true; + + // This command is started with the first colour ready to transfer. + Command::cycles = 32; + Command::access = logical ? Command::AccessType::PlotPoint : Command::AccessType::WriteByte; + } + + void advance() final { + switch(Command::access) { + default: break; + + case Command::AccessType::WaitForColourReceipt: + Command::cycles = 32; + Command::access = logical ? Command::AccessType::PlotPoint : Command::AccessType::WriteByte; + break; + + case Command::AccessType::WriteByte: + case Command::AccessType::PlotPoint: + Command::cycles = 0; + Command::access = Command::AccessType::WaitForColourReceipt; + if(Rectangle::advance_pixel()) { + Command::cycles = 64; + // TODO: I'm not sure this will be honoured per the outer wrapping. + } + break; + } + } +}; + +// MARK: - Rectangular moves within VRAM. + +enum class MoveType { + Logical, + HighSpeed, + YOnly, +}; + +template struct Move: public Rectangle { + static constexpr bool is_logical = type == MoveType::Logical; + static constexpr bool is_y_only = type == MoveType::YOnly; + using RectangleBase = Rectangle; + + Move(CommandContext &context, ModeDescription &mode_description) : RectangleBase(context, mode_description) { + Command::access = is_logical ? Command::AccessType::CopyPoint : Command::AccessType::CopyByte; + Command::cycles = is_y_only ? 0 : 64; + Command::y_only = is_y_only; + } + + void advance() final { + Command::cycles = is_y_only ? 40 : 64; + if(RectangleBase::advance_pixel()) { + Command::cycles += is_y_only ? 0 : 64; + } + } +}; + +// MARK: - Rectangular fills. + +template struct Fill: public Rectangle { + using RectangleBase = Rectangle; + + Fill(CommandContext &context, ModeDescription &mode_description) : RectangleBase(context, mode_description) { + Command::cycles = logical ? 64 : 56; + Command::access = logical ? Command::AccessType::PlotPoint : Command::AccessType::WriteByte; + } + + void advance() final { + Command::cycles = logical ? 72 : 48; + if(RectangleBase::advance_pixel()) { + Command::cycles += logical ? 64 : 56; + } + } +}; + +} +} + +#endif /* YamahaCommands_hpp */ diff --git a/Components/RP5C01/RP5C01.cpp b/Components/RP5C01/RP5C01.cpp index 8521f7989..3caf220b8 100644 --- a/Components/RP5C01/RP5C01.cpp +++ b/Components/RP5C01/RP5C01.cpp @@ -27,7 +27,7 @@ RP5C01::RP5C01(HalfCycles clock_rate) : clock_rate_(clock_rate) { day_of_the_week_ = time_date->tm_wday; day_ = time_date->tm_mday; month_ = time_date->tm_mon; - year_ = time_date->tm_year % 100; + year_ = (time_date->tm_year + 20) % 100; // This is probably MSX specific; rethink if/when other machines use this chip. leap_year_ = time_date->tm_year % 4; } @@ -171,19 +171,19 @@ void RP5C01::write(int address, uint8_t value) { } break; // Day of the week. - case Reg(0, 0x06): day_of_the_week_ = value % 7; break; + case Reg(0, 0x06): day_of_the_week_ = value % 7; break; // Day. - case Reg(0, 0x07): TwoDigitEncoder::encode<0>(day_, value); break; - case Reg(0, 0x08): TwoDigitEncoder::encode<1>(day_, value & 3); break; + case Reg(0, 0x07): TwoDigitEncoder::encode<0>(day_, value); break; + case Reg(0, 0x08): TwoDigitEncoder::encode<1>(day_, value & 3); break; // Month. - case Reg(0, 0x09): TwoDigitEncoder::encode<0>(month_, value); break; - case Reg(0, 0x0a): TwoDigitEncoder::encode<1>(month_, value & 1); break; + case Reg(0, 0x09): TwoDigitEncoder::encode<0>(month_, (value - 1)); break; + case Reg(0, 0x0a): TwoDigitEncoder::encode<1>(month_, (value - 1) & 1); break; // Year. - case Reg(0, 0x0b): TwoDigitEncoder::encode<0>(year_, value); break; - case Reg(0, 0x0c): TwoDigitEncoder::encode<1>(year_, value); break; + case Reg(0, 0x0b): TwoDigitEncoder::encode<0>(year_, value); break; + case Reg(0, 0x0c): TwoDigitEncoder::encode<1>(year_, value); break; // TODO: alarm minutes. case Reg(1, 0x02): @@ -271,10 +271,10 @@ uint8_t RP5C01::read(int address) { case Reg(0, 0x08): value = TwoDigitEncoder::decode<1>(day_); break; // Month. - case Reg(0, 0x09): value = TwoDigitEncoder::decode<0>(month_); break; - case Reg(0, 0x0a): value = TwoDigitEncoder::decode<1>(month_); break; + case Reg(0, 0x09): value = TwoDigitEncoder::decode<0>(month_ + 1); break; + case Reg(0, 0x0a): value = TwoDigitEncoder::decode<1>(month_ + 1); break; - // Year; + // Year. case Reg(0, 0x0b): value = TwoDigitEncoder::decode<0>(year_); break; case Reg(0, 0x0c): value = TwoDigitEncoder::decode<1>(year_); break; diff --git a/Machines/ColecoVision/ColecoVision.cpp b/Machines/ColecoVision/ColecoVision.cpp index 8349bbef6..19ba93adb 100644 --- a/Machines/ColecoVision/ColecoVision.cpp +++ b/Machines/ColecoVision/ColecoVision.cpp @@ -175,19 +175,19 @@ class ConcreteMachine: } void set_scan_target(Outputs::Display::ScanTarget *scan_target) final { - vdp_->set_scan_target(scan_target); + vdp_.last_valid()->set_scan_target(scan_target); } Outputs::Display::ScanStatus get_scaled_scan_status() const final { - return vdp_->get_scaled_scan_status(); + return vdp_.last_valid()->get_scaled_scan_status(); } void set_display_type(Outputs::Display::DisplayType display_type) final { - vdp_->set_display_type(display_type); + vdp_.last_valid()->set_display_type(display_type); } Outputs::Display::DisplayType get_display_type() const final { - return vdp_->get_display_type(); + return vdp_.last_valid()->get_display_type(); } Outputs::Speaker::Speaker *get_speaker() final { diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp index 52af973ef..afa84e143 100644 --- a/Machines/MSX/MSX.cpp +++ b/Machines/MSX/MSX.cpp @@ -319,11 +319,11 @@ class ConcreteMachine: } void set_display_type(Outputs::Display::DisplayType display_type) final { - vdp_->set_display_type(display_type); + vdp_.last_valid()->set_display_type(display_type); } Outputs::Display::DisplayType get_display_type() const final { - return vdp_->get_display_type(); + return vdp_.last_valid()->get_display_type(); } Outputs::Speaker::Speaker *get_speaker() final { @@ -568,6 +568,11 @@ class ConcreteMachine: case CPU::Z80::PartialMachineCycle::Input: switch(address & 0xff) { + case 0x9a: case 0x9b: + if constexpr (vdp_model() == TI::TMS::TMS9918A) { + break; + } + [[fallthrough]]; case 0x98: case 0x99: *cycle.value = vdp_->read(address); z80_.set_interrupt_line(vdp_->get_interrupt_line()); @@ -591,7 +596,7 @@ class ConcreteMachine: break; default: - printf("Unhandled read %02x\n", address & 0xff); +// printf("Unhandled read %02x\n", address & 0xff); *cycle.value = 0xff; break; } @@ -600,6 +605,11 @@ class ConcreteMachine: case CPU::Z80::PartialMachineCycle::Output: { const int port = address & 0xff; switch(port) { + case 0x9a: case 0x9b: + if constexpr (vdp_model() == TI::TMS::TMS9918A) { + break; + } + [[fallthrough]]; case 0x98: case 0x99: vdp_->write(address, *cycle.value); z80_.set_interrupt_line(vdp_->get_interrupt_line()); diff --git a/Machines/MasterSystem/MasterSystem.cpp b/Machines/MasterSystem/MasterSystem.cpp index ad4761926..8136049f6 100644 --- a/Machines/MasterSystem/MasterSystem.cpp +++ b/Machines/MasterSystem/MasterSystem.cpp @@ -181,24 +181,27 @@ template class ConcreteMachine: } void set_scan_target(Outputs::Display::ScanTarget *scan_target) final { - vdp_->set_tv_standard( + vdp_.last_valid()->set_tv_standard( (region_ == Target::Region::Europe) ? TI::TMS::TVStandard::PAL : TI::TMS::TVStandard::NTSC); - time_until_debounce_ = vdp_->get_time_until_line(-1); - vdp_->set_scan_target(scan_target); + // Doing the following would be technically correct, but isn't + // especially thread-safe and won't make a substantial difference. +// time_until_debounce_ = vdp_->get_time_until_line(-1); + + vdp_.last_valid()->set_scan_target(scan_target); } Outputs::Display::ScanStatus get_scaled_scan_status() const final { - return vdp_->get_scaled_scan_status(); + return vdp_.last_valid()->get_scaled_scan_status(); } void set_display_type(Outputs::Display::DisplayType display_type) final { - vdp_->set_display_type(display_type); + vdp_.last_valid()->set_display_type(display_type); } Outputs::Display::DisplayType get_display_type() const final { - return vdp_->get_display_type(); + return vdp_.last_valid()->get_display_type(); } Outputs::Speaker::Speaker *get_speaker() final { @@ -312,7 +315,7 @@ template class ConcreteMachine: case 0x00: // i.e. even ports less than 0x40. if constexpr (is_master_system(model)) { // TODO: Obey the RAM enable. - LOG("Memory control: " << PADHEX(2) << memory_control_); + LOG("Memory control: " << PADHEX(2) << +memory_control_); memory_control_ = *cycle.value; page_cartridge(); } diff --git a/Numeric/RegisterSizes.hpp b/Numeric/RegisterSizes.hpp index c0a86b92f..669227e9c 100644 --- a/Numeric/RegisterSizes.hpp +++ b/Numeric/RegisterSizes.hpp @@ -19,7 +19,7 @@ namespace CPU { /// Provides access to all intermediate parts of a larger int. -template union alignas(Full) RegisterPair { +template union alignas(Full) alignas(Half) RegisterPair { RegisterPair(Full v) : full(v) {} RegisterPair() {} diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 625438274..09469e660 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -1253,6 +1253,8 @@ 4B2530F3244E6773007980BF /* fm.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = fm.json; sourceTree = ""; }; 4B262BFF29691F55002EC0F7 /* PersonalityTraits.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PersonalityTraits.hpp; sourceTree = ""; }; 4B2A332C1DB86821002876E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/OricOptions.xib"; sourceTree = SOURCE_ROOT; }; + 4B2A3B5A29993DFA007CE366 /* Storage.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Storage.hpp; sourceTree = ""; }; + 4B2A3B5B29995FF6007CE366 /* LineBuffer.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LineBuffer.hpp; sourceTree = ""; }; 4B2A53901D117D36003C6002 /* CSAudioQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSAudioQueue.h; sourceTree = ""; }; 4B2A53911D117D36003C6002 /* CSAudioQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSAudioQueue.m; sourceTree = ""; }; 4B2A53951D117D36003C6002 /* CSMachine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSMachine.h; sourceTree = ""; }; @@ -2220,6 +2222,8 @@ 4BF0BC67297108D100CCA2B5 /* MemorySlotHandler.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MemorySlotHandler.cpp; sourceTree = ""; }; 4BF0BC6F2973318E00CCA2B5 /* RP5C01.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = RP5C01.cpp; sourceTree = ""; }; 4BF0BC702973318E00CCA2B5 /* RP5C01.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = RP5C01.hpp; sourceTree = ""; }; + 4BF0BC732982E54700CCA2B5 /* YamahaCommands.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = YamahaCommands.hpp; sourceTree = ""; }; + 4BF0BC742982E6D300CCA2B5 /* AccessEnums.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AccessEnums.hpp; sourceTree = ""; }; 4BF40A5525424C770033EA39 /* LanguageCardSwitches.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LanguageCardSwitches.hpp; sourceTree = ""; }; 4BF40A5A254263140033EA39 /* AuxiliaryMemorySwitches.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AuxiliaryMemorySwitches.hpp; sourceTree = ""; }; 4BF437EC209D0F7E008CBD6B /* SegmentParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SegmentParser.cpp; sourceTree = ""; }; @@ -4757,10 +4761,14 @@ children = ( 4B43983829620FB1006B0BFC /* 9918.cpp */, 4BD388411FE34E010042B588 /* 9918Base.hpp */, + 4BF0BC742982E6D300CCA2B5 /* AccessEnums.hpp */, 4B43983C29621024006B0BFC /* ClockConverter.hpp */, 4B43983F2967459B006B0BFC /* Draw.hpp */, 4B43983E29628538006B0BFC /* Fetch.hpp */, + 4B2A3B5B29995FF6007CE366 /* LineBuffer.hpp */, 4B262BFF29691F55002EC0F7 /* PersonalityTraits.hpp */, + 4B2A3B5A29993DFA007CE366 /* Storage.hpp */, + 4BF0BC732982E54700CCA2B5 /* YamahaCommands.hpp */, ); path = Implementation; sourceTree = ""; diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme index 474fa352e..3534e9926 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme +++ b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme @@ -1,7 +1,7 @@ + version = "1.3"> diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift index f4d3deccc..669b49ab3 100644 --- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift @@ -63,11 +63,6 @@ class MachineDocument: return "MachineDocument" } - convenience init(type typeName: String) throws { - self.init() - self.fileType = typeName - } - override func read(from url: URL, ofType typeName: String) throws { if let analyser = CSStaticAnalyser(fileAt: url) { self.displayName = analyser.displayName diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h index 4f96141f6..ddc82dd54 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h @@ -112,6 +112,11 @@ typedef NS_ENUM(NSInteger, CSMachineVic20Region) { CSMachineVic20RegionJapanese, }; +typedef NS_ENUM(NSInteger, CSMachineMSXModel) { + CSMachineMSXModelMSX1, + CSMachineMSXModelMSX2, +}; + typedef NS_ENUM(NSInteger, CSMachineMSXRegion) { CSMachineMSXRegionAmerican, CSMachineMSXRegionEuropean, @@ -132,7 +137,7 @@ typedef int Kilobytes; - (instancetype)initWithElectronDFS:(BOOL)dfs adfs:(BOOL)adfs ap6:(BOOL)ap6 sidewaysRAM:(BOOL)sidewaysRAM; - (instancetype)initWithEnterpriseModel:(CSMachineEnterpriseModel)model speed:(CSMachineEnterpriseSpeed)speed exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion dos:(CSMachineEnterpriseDOS)dos; - (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model; -- (instancetype)initWithMSXRegion:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive; +- (instancetype)initWithMSXModel:(CSMachineMSXModel)model region:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive; - (instancetype)initWithOricModel:(CSMachineOricModel)model diskInterface:(CSMachineOricDiskInterface)diskInterface; - (instancetype)initWithSpectrumModel:(CSMachineSpectrumModel)model; - (instancetype)initWithVic20Region:(CSMachineVic20Region)region memorySize:(Kilobytes)memorySize hasC1540:(BOOL)hasC1540; diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm index 964dd8612..2ef4dc36c 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm @@ -229,7 +229,7 @@ return self; } -- (instancetype)initWithMSXRegion:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive { +- (instancetype)initWithMSXModel:(CSMachineMSXModel)model region:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive { self = [super init]; if(self) { using Target = Analyser::Static::MSX::Target; @@ -240,6 +240,10 @@ case CSMachineMSXRegionEuropean: target->region = Target::Region::Europe; break; case CSMachineMSXRegionJapanese: target->region = Target::Region::Japan; break; } + switch(model) { + case CSMachineMSXModelMSX1: target->model = Target::Model::MSX1; break; + case CSMachineMSXModelMSX2: target->model = Target::Model::MSX2; break; + } _targets.push_back(std::move(target)); } return self; diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib index a1a5ebc4e..d0557d959 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib +++ b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib @@ -1,8 +1,8 @@ - + - + @@ -313,13 +313,13 @@ Gw - + - - + + @@ -575,18 +575,18 @@ Gw - + - + @@ -600,22 +600,48 @@ Gw - + + + + + + + + + + + + + + + + + + + + + + + + + - + + + @@ -966,7 +992,7 @@ Gw - + @@ -991,6 +1017,7 @@ Gw + diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift index b70d4c3df..2d37addc2 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift +++ b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift @@ -54,6 +54,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { @IBOutlet var macintoshModelTypeButton: NSPopUpButton! // MARK: - MSX properties + @IBOutlet var msxModelButton: NSPopUpButton! @IBOutlet var msxRegionButton: NSPopUpButton! @IBOutlet var msxHasDiskDriveButton: NSButton! @@ -134,6 +135,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { macintoshModelTypeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.macintoshModel")) // MSX settings + msxModelButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxModel")) msxRegionButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxRegion")) msxHasDiskDriveButton.state = standardUserDefaults.bool(forKey: "new.msxDiskDrive") ? .on : .off @@ -198,6 +200,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { standardUserDefaults.set(macintoshModelTypeButton.selectedTag(), forKey: "new.macintoshModel") // MSX settings + standardUserDefaults.set(msxModelButton.selectedTag(), forKey: "new.msxModel") standardUserDefaults.set(msxRegionButton.selectedTag(), forKey: "new.msxRegion") standardUserDefaults.set(msxHasDiskDriveButton.state == .on, forKey: "new.msxDiskDrive") @@ -355,15 +358,20 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { case "msx": let hasDiskDrive = msxHasDiskDriveButton.state == .on + var region: CSMachineMSXRegion switch msxRegionButton.selectedTag() { - case 2: - return CSStaticAnalyser(msxRegion: .japanese, hasDiskDrive: hasDiskDrive) - case 1: - return CSStaticAnalyser(msxRegion: .american, hasDiskDrive: hasDiskDrive) - case 0: fallthrough - default: - return CSStaticAnalyser(msxRegion: .european, hasDiskDrive: hasDiskDrive) + case 2: region = .japanese + case 1: region = .american + case 0: fallthrough + default: region = .european } + var model: CSMachineMSXModel + switch msxModelButton.selectedTag() { + case 2: model = .MSX2 + case 1: fallthrough + default: model = .MSX1 + } + return CSStaticAnalyser(msxModel: model, region: region, hasDiskDrive: hasDiskDrive) case "oric": var diskInterface: CSMachineOricDiskInterface = .none