mirror of
https://github.com/TomHarte/CLK.git
synced 2024-11-23 03:32:32 +00:00
Merge pull request #685 from TomHarte/LoadDelay
Adds a delay on load following DE
This commit is contained in:
commit
77fe14cdb3
@ -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<uint16_t *>(ram_.data()), ram_.size());
|
||||
|
@ -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 <horizontal.length);
|
||||
}
|
||||
|
||||
// Expected vertical order of events: reset blank, enable display, disable display, enable blank (at least 50 before end of line), end of line
|
||||
const auto vertical = vertical_parameters(Video::FieldFrequency(c));
|
||||
@ -87,8 +94,10 @@ struct Checker {
|
||||
} checker;
|
||||
#endif
|
||||
|
||||
const int de_delay_period = 28*2; // Number of half cycles after DE that observed DE changes.
|
||||
const int vsync_x_position = 54*2; // Horizontal cycle on which vertical sync changes happen.
|
||||
const int de_delay_period = CYCLE(28); // Number of half cycles after DE that observed DE changes.
|
||||
const int vsync_x_position = CYCLE(54); // Horizontal cycle on which vertical sync changes happen.
|
||||
const int hsync_start = CYCLE(48); // Cycles before end of line when hsync starts.
|
||||
const int hsync_end = CYCLE(8); // Cycles before end of line when hsync ends.
|
||||
|
||||
// "VSYNC starts 104 cycles after the start of the previous line's HSYNC, so that's 4 cycles before DE would be activated. ";
|
||||
// hsync is at -50, so that's +54
|
||||
@ -96,6 +105,7 @@ const int vsync_x_position = 54*2; // Horizontal cycle on which vertical sync ch
|
||||
}
|
||||
|
||||
Video::Video() :
|
||||
deferrer_([=] (HalfCycles duration) { advance(duration); }),
|
||||
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red4Green4Blue4),
|
||||
shifter_(crt_, palette_) {
|
||||
|
||||
@ -113,6 +123,10 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
}
|
||||
|
||||
void Video::run_for(HalfCycles duration) {
|
||||
deferrer_.run_for(duration);
|
||||
}
|
||||
|
||||
void Video::advance(HalfCycles duration) {
|
||||
const auto horizontal_timings = horizontal_parameters(field_frequency_);
|
||||
const auto vertical_timings = vertical_parameters(field_frequency_);
|
||||
int integer_duration = int(duration.as_integral());
|
||||
@ -145,10 +159,11 @@ void Video::run_for(HalfCycles duration) {
|
||||
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);
|
||||
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
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
#include "../../../Outputs/CRT/CRT.hpp"
|
||||
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
||||
#include "../../../ClockReceiver/DeferredQueue.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
@ -103,6 +104,9 @@ class Video {
|
||||
Range get_memory_access_range();
|
||||
|
||||
private:
|
||||
void advance(HalfCycles duration);
|
||||
DeferredQueue<HalfCycles> 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:
|
||||
|
Loading…
Reference in New Issue
Block a user