diff --git a/Machines/Atari/ST/AtariST.cpp b/Machines/Atari/ST/AtariST.cpp index 322d35c7e..a9e8697c9 100644 --- a/Machines/Atari/ST/AtariST.cpp +++ b/Machines/Atari/ST/AtariST.cpp @@ -67,7 +67,7 @@ class ConcreteMachine: speaker_(ay_), ikbd_(keyboard_acia_->transmit, keyboard_acia_->receive) { set_clock_rate(CLOCK_RATE); - speaker_.set_input_rate(CLOCK_RATE / 4); + speaker_.set_input_rate(float(CLOCK_RATE) / 4.0f); ram_.resize(512 * 1024); // i.e. 512kb video_->set_ram(reinterpret_cast(ram_.data()), ram_.size()); diff --git a/Machines/Atari/ST/Video.cpp b/Machines/Atari/ST/Video.cpp index 6fa3a9aa0..22e41770b 100644 --- a/Machines/Atari/ST/Video.cpp +++ b/Machines/Atari/ST/Video.cpp @@ -38,6 +38,7 @@ const VerticalParams &vertical_parameters(Video::FieldFrequency frequency) { } +#define CYCLE(x) ((x) * 2) /*! Defines the horizontal counts at which mode-specific events will occur: horizontal enable being set and being reset, blank being set and reset, and the @@ -58,9 +59,9 @@ const struct HorizontalParams { const int length; } horizontal_params[3] = { - {56*2, 376*2, 450*2, 28*2, 512*2}, - {52*2, 372*2, 450*2, 24*2, 508*2}, - {4*2, 164*2, 184*2, 2*2, 224*2} + {CYCLE(56), CYCLE(376), CYCLE(450), CYCLE(28), CYCLE(512)}, + {CYCLE(52), CYCLE(372), CYCLE(450), CYCLE(24), CYCLE(508)}, + {CYCLE(4), CYCLE(164), CYCLE(999), CYCLE(999), CYCLE(224)} // 72Hz mode doesn't set or reset blank. }; const HorizontalParams &horizontal_parameters(Video::FieldFrequency frequency) { @@ -73,10 +74,16 @@ struct Checker { for(int c = 0; c < 3; ++c) { // Expected horizontal order of events: reset blank, enable display, disable display, enable blank (at least 50 before end of line), end of line const auto horizontal = horizontal_parameters(Video::FieldFrequency(c)); - assert(horizontal.reset_blank < horizontal.set_enable); - assert(horizontal.set_enable < horizontal.reset_enable); - assert(horizontal.reset_enable < horizontal.set_blank); - assert(horizontal.set_blank+50 < horizontal.length); + + if(c < 2) { + assert(horizontal.reset_blank < horizontal.set_enable); + assert(horizontal.set_enable < horizontal.reset_enable); + assert(horizontal.reset_enable < horizontal.set_blank); + assert(horizontal.set_blank+50 < horizontal.length); + } else { + assert(horizontal.set_enable < horizontal.reset_enable); + assert(horizontal.set_enable+50 x_) next_event = std::min(next_event, horizontal_timings.set_blank); if(horizontal_timings.reset_enable > x_) next_event = std::min(next_event, horizontal_timings.reset_enable); if(horizontal_timings.set_enable > x_) next_event = std::min(next_event, horizontal_timings.set_enable); + if(next_load_toggle_ > x_) next_event = std::min(next_event, next_load_toggle_); // Check for events that are relative to existing latched state. - if(line_length_ - 50*2 > x_) next_event = std::min(next_event, line_length_ - 50*2); - if(line_length_ - 10*2 > x_) next_event = std::min(next_event, line_length_ - 10*2); + if(line_length_ - hsync_start > x_) next_event = std::min(next_event, line_length_ - hsync_start); + if(line_length_ - hsync_end > x_) next_event = std::min(next_event, line_length_ - hsync_end); // Also, a vertical sync event might intercede. if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && x_ < vsync_x_position && next_event >= vsync_x_position) { @@ -159,18 +174,42 @@ void Video::run_for(HalfCycles duration) { const int run_length = std::min(integer_duration, next_event - x_); const bool display_enable = vertical_.enable && horizontal_.enable; + // Ensure proper fetching irrespective of the output. + if(load_) { + const int since_load = x_ - load_base_; + + // There will be pixels this line, subject to the shifter pipeline. + // Divide into 8-[half-]cycle windows; at the start of each window fetch a word, + // and during the rest of the window, shift out. + int start_column = since_load >> 3; + const int end_column = (since_load + run_length) >> 3; + + while(start_column != end_column) { + data_latch_[data_latch_position_] = ram_[current_address_ & 262143]; + data_latch_position_ = (data_latch_position_ + 1) & 127; + ++current_address_; + ++start_column; + } + } + + // TODO: if I'm asserting that sync and blank override the shifter (but, presumably, + // the shifter keeps shifting), then output_sync and output_blank need to have an effect + // inside the shifter on the temporary register values. + if(horizontal_.sync || vertical_.sync) { shifter_.output_sync(run_length); } else if(horizontal_.blank || vertical_.blank) { shifter_.output_blank(run_length); - } else if(!vertical_.enable || !horizontal_.enable) { + } else if(!load_) { shifter_.output_border(run_length, output_bpp_); } else { + const int since_load = x_ - load_base_; + // There will be pixels this line, subject to the shifter pipeline. // Divide into 8-[half-]cycle windows; at the start of each window fetch a word, // and during the rest of the window, shift out. - int start_column = x_ >> 3; - const int end_column = (x_ + run_length) >> 3; + int start_column = since_load >> 3; + const int end_column = (since_load + run_length) >> 3; // Rules obeyed below: // @@ -182,32 +221,32 @@ void Video::run_for(HalfCycles duration) { shifter_.output_pixels(run_length, output_bpp_); } else { // Continue the current column if partway across. - if(x_&7) { + if(since_load&7) { // If at least one column boundary is crossed, complete this column. - shifter_.output_pixels(8 - (x_ & 7), output_bpp_); + shifter_.output_pixels(8 - (since_load & 7), output_bpp_); ++start_column; // This starts a new column, so latch a new word. - latch_word(); + push_latched_data(); } // Run for all columns that have their starts in this time period. int complete_columns = end_column - start_column; while(complete_columns--) { shifter_.output_pixels(8, output_bpp_); - latch_word(); + push_latched_data(); } // Output the start of the next column, if necessary. - if((x_ + run_length) & 7) { - shifter_.output_pixels((x_ + run_length) & 7, output_bpp_); + if((since_load + run_length) & 7) { + shifter_.output_pixels((since_load + run_length) & 7, output_bpp_); } } } // Check for whether line length should have been latched during this run. - if(x_ <= 54*2 && (x_ + run_length) > 54*2) line_length_ = horizontal_timings.length; + if(x_ <= CYCLE(54) && (x_ + run_length) > CYCLE(54)) line_length_ = horizontal_timings.length; // Make a decision about vertical state on cycle 502. - if(x_ <= 502*2 && (x_ + run_length) > 502*2) { + if(x_ <= CYCLE(502) && (x_ + run_length) > CYCLE(502)) { next_y_ = y_ + 1; next_vertical_ = vertical_; next_vertical_.sync_schedule = VerticalState::SyncSchedule::None; @@ -224,6 +263,7 @@ void Video::run_for(HalfCycles duration) { } else if(next_y_ == vertical_timings.height) { next_y_ = 0; current_address_ = base_address_ >> 1; + reset_fifo(); // TODO: remove this, I think, once otherwise stable. // Consider a shout out to the range observer. if(previous_base_address_ != base_address_) { @@ -248,8 +288,13 @@ void Video::run_for(HalfCycles duration) { else if(horizontal_timings.set_blank == x_) horizontal_.blank = true; else if(horizontal_timings.reset_enable == x_) horizontal_.enable = false; else if(horizontal_timings.set_enable == x_) horizontal_.enable = true; - else if(line_length_ - 50*2 == x_) horizontal_.sync = true; - else if(line_length_ - 10*2 == x_) horizontal_.sync = false; + else if(line_length_ - hsync_start == x_) { horizontal_.sync = true; horizontal_.enable = false; } + else if(line_length_ - hsync_end == x_) horizontal_.sync = false; + else if(next_load_toggle_ == x_) { + next_load_toggle_ = -1; + load_ ^= true; + load_base_ = x_; + } // Check vertical events. if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && x_ == vsync_x_position) { @@ -268,26 +313,32 @@ void Video::run_for(HalfCycles duration) { // Chuck any deferred output changes into the queue. const bool next_display_enable = vertical_.enable && horizontal_.enable; if(display_enable != next_display_enable) { + // Schedule change in outwardly-visible DE line. add_event(de_delay_period - integer_duration, next_display_enable ? Event::Type::SetDisplayEnable : Event::Type::ResetDisplayEnable); + + // Schedule change in inwardly-visible effect. + next_load_toggle_ = x_ + 8; // 4 cycles = 8 half-cycles } } } -void Video::latch_word() { - data_latch_[data_latch_position_] = ram_[current_address_ & 262143]; - ++current_address_; - ++data_latch_position_; - if(data_latch_position_ == 4) { - data_latch_position_ = 0; +void Video::push_latched_data() { + data_latch_read_position_ = (data_latch_read_position_ + 1) & 127; + + if(!(data_latch_read_position_ & 3)) { shifter_.load( - (uint64_t(data_latch_[0]) << 48) | - (uint64_t(data_latch_[1]) << 32) | - (uint64_t(data_latch_[2]) << 16) | - uint64_t(data_latch_[3]) + (uint64_t(data_latch_[(data_latch_read_position_ - 4) & 127]) << 48) | + (uint64_t(data_latch_[(data_latch_read_position_ - 3) & 127]) << 32) | + (uint64_t(data_latch_[(data_latch_read_position_ - 2) & 127]) << 16) | + uint64_t(data_latch_[(data_latch_read_position_ - 1) & 127]) ); } } +void Video::reset_fifo() { + data_latch_read_position_ = data_latch_position_ = 0; +} + bool Video::hsync() { return horizontal_.sync; } @@ -342,8 +393,8 @@ HalfCycles Video::get_next_sequence_point() { } // Test for beginning and end of horizontal sync. - if(x_ < line_length_ - 50*2) event_time = std::min(line_length_ - 50*2, event_time); - else if(x_ < line_length_ - 10*2) event_time = std::min(line_length_ - 10*2, event_time); + if(x_ < line_length_ - hsync_start) event_time = std::min(line_length_ - hsync_start, event_time); + else if(x_ < line_length_ - hsync_end) event_time = std::min(line_length_ - hsync_end, event_time); // It wasn't any of those, so as a temporary expedient, just supply end of line. return HalfCycles(event_time - x_); @@ -384,8 +435,11 @@ void Video::write(int address, uint16_t value) { // Sync mode and pixel mode. case 0x05: - sync_mode_ = value; - update_output_mode(); + // Writes to sync mode have a one-cycle delay in effect. + deferrer_.defer(HalfCycles(2), [=] { + sync_mode_ = value; + update_output_mode(); + }); break; case 0x30: video_mode_ = value; @@ -406,24 +460,26 @@ void Video::write(int address, uint16_t value) { } void Video::update_output_mode() { + const auto old_bpp_ = output_bpp_; + // If this is black and white mode, that's that. switch((video_mode_ >> 8) & 3) { case 0: output_bpp_ = OutputBpp::Four; break; case 1: output_bpp_ = OutputBpp::Two; break; - - // 1bpp mode ignores the otherwise-programmed frequency. default: - case 2: - output_bpp_ = OutputBpp::One; - field_frequency_ = FieldFrequency::SeventyTwo; - return; + case 2: output_bpp_ = OutputBpp::One; break; } -// const auto old_frequency = field_frequency_; - field_frequency_ = (sync_mode_ & 0x200) ? FieldFrequency::Fifty : FieldFrequency::Sixty; -// if(field_frequency_ != old_frequency) { -// printf("%d, %d -> %d\n", x_, y_, field_frequency_); -// } + // 1bpp mode ignores the otherwise-programmed frequency. + if(output_bpp_ == OutputBpp::One) { + field_frequency_ = FieldFrequency::SeventyTwo; + } else { + field_frequency_ = (sync_mode_ & 0x200) ? FieldFrequency::Fifty : FieldFrequency::Sixty; + } + if(output_bpp_ != old_bpp_) { + // "the 71-Hz-switch does something like a shifter-reset." (and some people use a high-low resolutions switch instead) + reset_fifo(); + } } // MARK: - The shifter diff --git a/Machines/Atari/ST/Video.hpp b/Machines/Atari/ST/Video.hpp index fc8a4b476..971d31ac1 100644 --- a/Machines/Atari/ST/Video.hpp +++ b/Machines/Atari/ST/Video.hpp @@ -11,6 +11,7 @@ #include "../../../Outputs/CRT/CRT.hpp" #include "../../../ClockReceiver/ClockReceiver.hpp" +#include "../../../ClockReceiver/DeferredQueue.hpp" #include @@ -103,6 +104,9 @@ class Video { Range get_memory_access_range(); private: + void advance(HalfCycles duration); + DeferredQueue deferrer_; + Outputs::CRT::CRT crt_; RangeObserver *range_observer_ = nullptr; @@ -116,6 +120,9 @@ class Video { uint16_t line_buffer_[256]; int x_ = 0, y_ = 0, next_y_ = 0; + int next_load_toggle_ = -1; + bool load_ = false; + int load_base_ = 0; uint16_t video_mode_ = 0; uint16_t sync_mode_ = 0; @@ -148,8 +155,11 @@ class Video { int line_length_ = 1024; int data_latch_position_ = 0; - uint16_t data_latch_[4] = {0, 0, 0, 0}; - void latch_word(); + int data_latch_read_position_ = 0; + uint16_t data_latch_[128]; + void push_latched_data(); + + void reset_fifo(); class Shifter { public: