// // Video.cpp // Clock Signal // // Created by Thomas Harte on 04/10/2019. // Copyright © 2019 Thomas Harte. All rights reserved. // #include "Video.hpp" #include "../../../Outputs/Log.hpp" #include #include #define CYCLE(x) ((x) * 2) using namespace Atari::ST; namespace { /*! Defines the line counts at which mode-specific events will occur: vertical enable being set and being reset, and the line on which the frame will end. */ const struct VerticalParams { const int set_enable; const int reset_enable; const int height; } vertical_params[3] = { {63, 263, 313}, // 47 rather than 63 on early machines. {34, 234, 263}, {51, 451, 500} // 72 Hz mode: who knows? }; /// @returns The correct @c VerticalParams for output at @c frequency. const VerticalParams &vertical_parameters(Video::FieldFrequency frequency) { return vertical_params[int(frequency)]; } /*! 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 intended length of this ine. The caller should: * latch line length at cycle 54 (TODO: also for 72Hz mode?); * at (line length - 50), start sync and reset enable (usually for the second time); * at (line length - 10), disable sync. */ const struct HorizontalParams { const int set_enable; const int reset_enable; const int set_blank; const int reset_blank; const int vertical_decision; LineLength length; } horizontal_params[3] = { {CYCLE(56), CYCLE(376), CYCLE(450), CYCLE(28), CYCLE(502), { CYCLE(512), CYCLE(464), CYCLE(504) }}, {CYCLE(52), CYCLE(372), CYCLE(450), CYCLE(24), CYCLE(502), { CYCLE(508), CYCLE(460), CYCLE(500) }}, {CYCLE(4), CYCLE(164), CYCLE(999), CYCLE(999), CYCLE(214), { CYCLE(224), CYCLE(194), CYCLE(212) }} // 72Hz mode doesn't set or reset blank. }; // Re: 'vertical_decision': // This is cycle 502 if in 50 or 60 Hz mode; in 70 Hz mode I've put it on cycle 214 // in order to be analogous to 50 and 60 Hz mode. I have no idea where it should // actually go. // // Ditto the horizontal sync timings for 72Hz are plucked out of thin air. const HorizontalParams &horizontal_parameters(Video::FieldFrequency frequency) { return horizontal_params[int(frequency)]; } #ifndef NDEBUG struct Checker { 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)); 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.length); } else { assert(horizontal.set_enable < horizontal.reset_enable); assert(horizontal.set_enable+50 = 0); while(integer_duration) { const auto horizontal_timings = horizontal_parameters(field_frequency_); const auto vertical_timings = vertical_parameters(field_frequency_); // Determine time to next event; this'll either be one of the ones informally scheduled in here, // or something from the deferral queue. // Seed next event to end of line. int next_event = line_length_.length; const int next_deferred_event = deferrer_.time_until_next_action().as(); if(next_deferred_event >= 0) next_event = std::min(next_event, next_deferred_event + x_); // Check the explicitly-placed events. if(horizontal_timings.reset_blank > x_) next_event = std::min(next_event, horizontal_timings.reset_blank); if(horizontal_timings.set_blank > 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); // Check for events that are relative to existing latched state. 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) { next_event = vsync_x_position; } // Determine current output mode and number of cycles to output for. const int run_length = std::min(integer_duration, next_event - x_); const bool display_enable = vertical_.enable && horizontal_.enable; const bool hsync = horizontal_.sync; const bool vsync = vertical_.sync; assert(run_length > 0); // 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; } } if(horizontal_.sync || vertical_.sync) { video_stream_.output(run_length, VideoStream::OutputMode::Sync); } else if(horizontal_.blank || vertical_.blank) { video_stream_.output(run_length, VideoStream::OutputMode::Blank); } else if(!load_) { video_stream_.output(run_length, VideoStream::OutputMode::Pixels); } 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 = since_load >> 3; const int end_column = (since_load + run_length) >> 3; // Rules obeyed below: // // Video fetches occur as the first act of business in a column. Each // fetch is then followed by 8 shift clocks. Whether or not the shifter // was reloaded by the fetch depends on the FIFO. if(start_column == end_column) { video_stream_.output(run_length, VideoStream::OutputMode::Pixels); } else { // Continue the current column if partway across. if(since_load&7) { // If at least one column boundary is crossed, complete this column. video_stream_.output(8 - (since_load & 7), VideoStream::OutputMode::Pixels); ++start_column; // This starts a new column, so latch a new 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--) { video_stream_.output(8, VideoStream::OutputMode::Pixels); push_latched_data(); } // Output the start of the next column, if necessary. if((since_load + run_length) & 7) { video_stream_.output((since_load + run_length) & 7, VideoStream::OutputMode::Pixels); } } } // Check for whether line length should have been latched during this run. if(x_ < line_length_latch_position && (x_ + run_length) >= line_length_latch_position) { line_length_ = horizontal_timings.length; } // Make a decision about vertical state on the appropriate cycle. if(x_ < horizontal_timings.vertical_decision && (x_ + run_length) >= horizontal_timings.vertical_decision) { next_y_ = y_ + 1; next_vertical_ = vertical_; next_vertical_.sync_schedule = VerticalState::SyncSchedule::None; // Use vertical_parameters to get parameters for the current output frequency; // quick note: things other than the total frame size are counted in terms // of the line they're evaluated on — i.e. the test is this line, not the next // one. The total height constraint is obviously whether the next one would be // too far. if(y_ == vertical_timings.set_enable) { next_vertical_.enable = true; } else if(y_ == vertical_timings.reset_enable) { next_vertical_.enable = false; } else if(next_y_ == vertical_timings.height - 2) { next_vertical_.sync_schedule = VerticalState::SyncSchedule::Begin; } else if(next_y_ == vertical_timings.height) { next_y_ = 0; } else if(y_ == 0) { next_vertical_.sync_schedule = VerticalState::SyncSchedule::End; } } // Apply the next event. x_ += run_length; assert(integer_duration >= run_length); integer_duration -= run_length; deferrer_.advance(HalfCycles(run_length)); // Check horizontal events; the first six are guaranteed to occur separately. if(horizontal_timings.reset_blank == x_) horizontal_.blank = false; 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_.hsync_start == x_) { horizontal_.sync = true; horizontal_.enable = false; } else if(line_length_.hsync_end == x_) horizontal_.sync = false; // Check vertical events. if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && x_ == vsync_x_position) { vertical_.sync = vertical_.sync_schedule == VerticalState::SyncSchedule::Begin; vertical_.enable &= !vertical_.sync; reset_fifo(); // TODO: remove this, probably, once otherwise stable? } // Check whether the terminating event was end-of-line; if so then advance // the vertical bits of state. if(x_ == line_length_.length) { x_ = 0; vertical_ = next_vertical_; y_ = next_y_; } // The address is reloaded during the entire period of vertical sync. // Cf. http://www.atari-forum.com/viewtopic.php?t=31954&start=50#p324730 if(vertical_.sync) { current_address_ = base_address_ >> 1; // Consider a shout out to the range observer. if(previous_base_address_ != base_address_) { previous_base_address_ = base_address_; if(range_observer_) { range_observer_->video_did_change_access_range(this); } } } // 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 load line. deferrer_.defer(load_delay_period, [this, next_display_enable] { this->load_ = next_display_enable; this->load_base_ = this->x_; }); // Schedule change in outwardly-visible DE line. deferrer_.defer(de_delay_period, [this, next_display_enable] { this->public_state_.display_enable = next_display_enable; }); } if(horizontal_.sync != hsync) { // Schedule change in outwardly-visible hsync line. deferrer_.defer(hsync_delay_period, [this, next_horizontal_sync = horizontal_.sync] { this->public_state_.hsync = next_horizontal_sync; }); } if(vertical_.sync != vsync) { // Schedule change in outwardly-visible hsync line. deferrer_.defer(vsync_delay_period, [this, next_vertical_sync = vertical_.sync] { this->public_state_.vsync = next_vertical_sync; }); } } } void Video::push_latched_data() { data_latch_read_position_ = (data_latch_read_position_ + 1) & 127; if(!(data_latch_read_position_ & 3)) { video_stream_.load( (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 public_state_.hsync; } bool Video::vsync() { return public_state_.vsync; } bool Video::display_enabled() { return public_state_.display_enable; } HalfCycles Video::get_next_sequence_point() { // The next sequence point will be whenever display_enabled, vsync or hsync next changes. // Sequence of events within a standard line: // // 1) blank disabled; // 2) display enabled; // 3) display disabled; // 4) blank enabled; // 5) sync enabled; // 6) sync disabled; // 7) end-of-line, potential vertical event. // // If this line has a vertical sync event on it, there will also be an event at cycle 30, // which will always falls somewhere between (1) and (4) but might or might not be in the // visible area. const auto horizontal_timings = horizontal_parameters(field_frequency_); int event_time = line_length_.length; // Worst case: report end of line. // If any events are pending, give the first of those the chance to be next. const auto next_deferred_item = deferrer_.time_until_next_action(); if(next_deferred_item != HalfCycles(-1)) { event_time = std::min(event_time, x_ + next_deferred_item.as()); } // If this is a vertically-enabled line, check for the display enable boundaries, + the standard delay. if(vertical_.enable) { if(x_ < horizontal_timings.set_enable + de_delay_period) { event_time = std::min(event_time, horizontal_timings.set_enable + de_delay_period); } else if(x_ < horizontal_timings.reset_enable + de_delay_period) { event_time = std::min(event_time, horizontal_timings.reset_enable + de_delay_period); } } // If a vertical sync event is scheduled, test for that. if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && (x_ < vsync_x_position)) { event_time = std::min(event_time, vsync_x_position); } // Test for beginning and end of horizontal sync. if(x_ < line_length_.hsync_start + hsync_delay_period) { event_time = std::min(line_length_.hsync_start + hsync_delay_period, event_time); } if(x_ < line_length_.hsync_end + hsync_delay_period) { event_time = std::min(line_length_.hsync_end + hsync_delay_period, event_time); } // Also factor in the line length latching time. if(x_ < line_length_latch_position) { event_time = std::min(line_length_latch_position, event_time); } // It wasn't any of those, just supply end of line. That's when the static_assert above assumes a visible hsync transition. return HalfCycles(event_time - x_); } // MARK: - IO dispatch uint16_t Video::read(int address) { address &= 0x3f; switch(address) { default: break; case 0x00: return uint16_t(0xff00 | (base_address_ >> 16)); case 0x01: return uint16_t(0xff00 | (base_address_ >> 8)); case 0x02: return uint16_t(0xff00 | (current_address_ >> 15)); // Current address is kept in word precision internally; case 0x03: return uint16_t(0xff00 | (current_address_ >> 7)); // the shifts here represent a conversion back to case 0x04: return uint16_t(0xff00 | (current_address_ << 1)); // byte precision. case 0x05: return sync_mode_ | 0xfcff; case 0x30: return video_mode_ | 0xfcff; case 0x20: case 0x21: case 0x22: case 0x23: case 0x24: case 0x25: case 0x26: case 0x27: case 0x28: case 0x29: case 0x2a: case 0x2b: case 0x2c: case 0x2d: case 0x2e: case 0x2f: return raw_palette_[address - 0x20]; } return 0xff; } void Video::write(int address, uint16_t value) { address &= 0x3f; switch(address) { default: break; // Start address. case 0x00: base_address_ = (base_address_ & 0x00ffff) | ((value & 0xff) << 16); break; case 0x01: base_address_ = (base_address_ & 0xff00ff) | ((value & 0xff) << 8); break; // Sync mode and pixel mode. case 0x05: // 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; update_output_mode(); break; // Palette. case 0x20: case 0x21: case 0x22: case 0x23: case 0x24: case 0x25: case 0x26: case 0x27: case 0x28: case 0x29: case 0x2a: case 0x2b: case 0x2c: case 0x2d: case 0x2e: case 0x2f: { if(address == 0x20) video_stream_.will_change_border_colour(); raw_palette_[address - 0x20] = value; uint8_t *const entry = reinterpret_cast(&palette_[address - 0x20]); entry[0] = uint8_t((value & 0x700) >> 7); entry[1] = uint8_t((value & 0x77) << 1); } break; } } 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; default: case 2: output_bpp_ = OutputBpp::One; break; } // 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(); video_stream_.set_bpp(output_bpp_); } // const int freqs[] = {50, 60, 72}; // printf("%d, %d -> %d [%d %d]\n", x_ / 2, y_, freqs[int(field_frequency_)], horizontal_.enable, vertical_.enable); } // MARK: - The shifter void Video::VideoStream::output(int duration, OutputMode mode) { // If this is a transition from sync to blank, actually transition to colour burst. if(output_mode_ == OutputMode::Sync && mode == OutputMode::Blank) { mode = OutputMode::ColourBurst; } // If this is seeming a transition from blank to colour burst, obey it only if/when // sufficient colour burst has been output. if(output_mode_ == OutputMode::Blank && mode == OutputMode::ColourBurst) { if(duration_ + duration >= 40) { const int overage = duration + duration_ - 40; duration_ = 40; generate(overage, OutputMode::ColourBurst, true); } else { mode = OutputMode::ColourBurst; } } // If this is a transition, or if we're doing pixels, output whatever has been accumulated. if(mode != output_mode_ || output_mode_ == OutputMode::Pixels) { generate(duration, output_mode_, mode != output_mode_); } else { duration_ += duration; } // Accumulate time in the current mode. output_mode_ = mode; } void Video::VideoStream::generate(int duration, OutputMode mode, bool is_terminal) { // Three of these are trivial; deal with them upfront. They don't care about the duration of // whatever is new, just about how much was accumulated prior to now. if(mode != OutputMode::Pixels) { switch(mode) { default: case OutputMode::Sync: crt_.output_sync(duration_*2); break; case OutputMode::Blank: crt_.output_blank(duration_*2); break; case OutputMode::ColourBurst: crt_.output_default_colour_burst(duration_*2); break; } // Reseed duration duration_ = duration; // The shifter should keep running, so throw away the proper amount of content. shift(duration_); return; } // If the shifter is empty, accumulate in duration_ a promise to draw border later. if(!output_shifter_) { if(pixel_pointer_) { flush_pixels(); } duration_ += duration; // If this is terminal, we'll need to draw now. But if it isn't, job done. if(is_terminal) { flush_border(); } return; } // There's definitely some pixels to convey, but perhaps there's some border first? if(duration_) { flush_border(); } // Time to do some pixels! output_pixels(duration); // If was terminal, make sure any transient storage is output. if(is_terminal) { flush_pixels(); } } void Video::VideoStream::will_change_border_colour() { // Flush the accumulated border if it'd be adversely affected. if(duration_ && output_mode_ == OutputMode::Pixels) { flush_border(); } } void Video::VideoStream::flush_border() { // Output colour 0 for the entirety of duration_ (or black, if this is 1bpp mode). uint16_t *const colour_pointer = reinterpret_cast(crt_.begin_data(1)); if(colour_pointer) *colour_pointer = (bpp_ != OutputBpp::One) ? palette_[0] : 0; crt_.output_level(duration_*2); duration_ = 0; } namespace { #if TARGET_RT_BIG_ENDIAN constexpr int upper = 0; #else constexpr int upper = 1; #endif } void Video::VideoStream::shift(int duration) { switch(bpp_) { case OutputBpp::One: output_shifter_ <<= (duration << 1); break; case OutputBpp::Two: while(duration--) { shifter_halves_[upper] = (shifter_halves_[upper] << 1) & 0xfffefffe; shifter_halves_[upper] |= (shifter_halves_[upper^1] & 0x80008000) >> 15; shifter_halves_[upper^1] = (shifter_halves_[upper^1] << 1) & 0xfffefffe; } break; case OutputBpp::Four: while(duration) { output_shifter_ = (output_shifter_ << 1) & 0xfffefffefffefffe; duration -= 2; } break; } } // TODO: turn this into a template on current BPP, perhaps? Would avoid reevaluation of the conditional. void Video::VideoStream::output_pixels(int duration) { constexpr int allocation_size = 352; // i.e. 320 plus a spare 32. // Convert from duration to pixels. int pixels = duration; switch(bpp_) { case OutputBpp::One: pixels <<= 1; break; default: break; case OutputBpp::Four: pixels >>= 1; break; } while(pixels) { // If no buffer is currently available, attempt to allocate one. if(!pixel_buffer_) { pixel_buffer_ = reinterpret_cast(crt_.begin_data(allocation_size, 2)); // Stop the loop if no buffer is available. if(!pixel_buffer_) break; } int pixels_to_draw = std::min(allocation_size - pixel_pointer_, pixels); pixels -= pixels_to_draw; switch(bpp_) { case OutputBpp::One: while(pixels_to_draw--) { pixel_buffer_[pixel_pointer_] = ((output_shifter_ >> 63) & 1) * 0xffff; output_shifter_ <<= 1; ++pixel_pointer_; } break; case OutputBpp::Two: while(pixels_to_draw--) { pixel_buffer_[pixel_pointer_] = palette_[ ((output_shifter_ >> 63) & 1) | ((output_shifter_ >> 46) & 2) ]; // This ensures that the top two words shift one to the left; // their least significant bits are fed from the most significant bits // of the bottom two words, respectively. shifter_halves_[upper] = (shifter_halves_[upper] << 1) & 0xfffefffe; shifter_halves_[upper] |= (shifter_halves_[upper^1] & 0x80008000) >> 15; shifter_halves_[upper^1] = (shifter_halves_[upper^1] << 1) & 0xfffefffe; ++pixel_pointer_; } break; case OutputBpp::Four: while(pixels_to_draw--) { pixel_buffer_[pixel_pointer_] = palette_[ ((output_shifter_ >> 63) & 1) | ((output_shifter_ >> 46) & 2) | ((output_shifter_ >> 29) & 4) | ((output_shifter_ >> 12) & 8) ]; output_shifter_ = (output_shifter_ << 1) & 0xfffefffefffefffe; ++pixel_pointer_; } break; } // Check whether the limit has been reached. if(pixel_pointer_ == allocation_size) { flush_pixels(); } } // If duration remains, that implies no buffer was available, so // just do the corresponding shifting and provide proper timing to the CRT. if(pixels) { int leftover_duration = pixels; switch(bpp_) { case OutputBpp::One: leftover_duration >>= 1; break; default: break; case OutputBpp::Four: leftover_duration <<= 1; break; } shift(leftover_duration); crt_.output_data(leftover_duration*2); } } void Video::VideoStream::flush_pixels() { // Flush only if there's something to flush. if(pixel_pointer_) { switch(bpp_) { case OutputBpp::One: crt_.output_data(pixel_pointer_); break; default: crt_.output_data(pixel_pointer_ << 1, size_t(pixel_pointer_)); break; case OutputBpp::Four: crt_.output_data(pixel_pointer_ << 2, size_t(pixel_pointer_)); break; } } pixel_pointer_ = 0; pixel_buffer_ = nullptr; } void Video::VideoStream::set_bpp(OutputBpp bpp) { // Terminate the allocated block of memory (if any). flush_pixels(); // Reset the shifter. // TODO: is flushing like this correct? output_shifter_ = 0; // Store the new BPP. bpp_ = bpp; } void Video::VideoStream::load(uint64_t value) { // In 1bpp mode, a 0 bit is white and a 1 bit is black. // Invert the input so that the 'just output the border colour // when the shifter is empty' optimisation works. if(bpp_ == OutputBpp::One) output_shifter_ = ~value; else output_shifter_ = value; } // MARK: - Range observer. Video::Range Video::get_memory_access_range() { Range range; range.low_address = uint32_t(previous_base_address_); range.high_address = range.low_address + 56994; // 56994 is pessimistic but unscientific, being derived from the resolution of the largest // fullscreen demo I could quickly find documentation of. TODO: calculate real number. return range; } void Video::set_range_observer(RangeObserver *observer) { range_observer_ = observer; observer->video_did_change_access_range(this); }