1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-06 01:28:57 +00:00

Merge pull request #745 from TomHarte/STMonochrome

Adds some amount of 1bpp/72Hz output support for the Atari ST.
This commit is contained in:
Thomas Harte 2020-02-02 17:46:36 -05:00 committed by GitHub
commit 0310f94f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 523 additions and 207 deletions

View File

@ -13,62 +13,68 @@
#include <vector>
/*!
A DeferredQueue maintains a list of ordered actions and the times at which
they should happen, and divides a total execution period up into the portions
that occur between those actions, triggering each action when it is reached.
Provides the logic to insert into and traverse a list of future scheduled items.
*/
template <typename TimeUnit> class DeferredQueue {
public:
/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
DeferredQueue(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {}
/*!
Schedules @c action to occur in @c delay units of time.
Actions must be scheduled in the order they will occur. It is undefined behaviour
to schedule them out of order.
*/
void defer(TimeUnit delay, const std::function<void(void)> &action) {
pending_actions_.emplace_back(delay, action);
}
/*!
Runs for @c length units of time.
The constructor-supplied target will be called with one or more periods that add up to @c length;
any scheduled actions will be called between periods.
*/
void run_for(TimeUnit length) {
// If there are no pending actions, just run for the entire length.
// This should be the normal branch.
if(pending_actions_.empty()) {
target_(length);
// Apply immediately if there's no delay (or a negative delay).
if(delay <= TimeUnit(0)) {
action();
return;
}
// Divide the time to run according to the pending actions.
while(length > TimeUnit(0)) {
TimeUnit next_period = pending_actions_.empty() ? length : std::min(length, pending_actions_[0].delay);
target_(next_period);
length -= next_period;
if(!pending_actions_.empty()) {
// Otherwise enqueue, having subtracted the delay for any preceding events,
// and subtracting from the subsequent, if any.
auto insertion_point = pending_actions_.begin();
while(insertion_point != pending_actions_.end() && insertion_point->delay < delay) {
delay -= insertion_point->delay;
++insertion_point;
}
if(insertion_point != pending_actions_.end()) {
insertion_point->delay -= delay;
}
off_t performances = 0;
for(auto &action: pending_actions_) {
action.delay -= next_period;
if(!action.delay) {
action.action();
++performances;
}
}
if(performances) {
pending_actions_.erase(pending_actions_.begin(), pending_actions_.begin() + performances);
pending_actions_.emplace(insertion_point, delay, action);
} else {
pending_actions_.emplace_back(delay, action);
}
}
/*!
@returns The amount of time until the next enqueued action will occur,
or TimeUnit(-1) if the queue is empty.
*/
TimeUnit time_until_next_action() {
if(pending_actions_.empty()) return TimeUnit(-1);
return pending_actions_.front().delay;
}
/*!
Advances the queue the specified amount of time, performing any actions it reaches.
*/
void advance(TimeUnit time) {
auto erase_iterator = pending_actions_.begin();
while(erase_iterator != pending_actions_.end()) {
erase_iterator->delay -= time;
if(erase_iterator->delay <= TimeUnit(0)) {
time = -erase_iterator->delay;
erase_iterator->action();
++erase_iterator;
} else {
break;
}
}
if(erase_iterator != pending_actions_.begin()) {
pending_actions_.erase(pending_actions_.begin(), erase_iterator);
}
}
private:
std::function<void(TimeUnit)> target_;
// The list of deferred actions.
struct DeferredAction {
TimeUnit delay;
@ -79,4 +85,40 @@ template <typename TimeUnit> class DeferredQueue {
std::vector<DeferredAction> pending_actions_;
};
/*!
A DeferredQueue maintains a list of ordered actions and the times at which
they should happen, and divides a total execution period up into the portions
that occur between those actions, triggering each action when it is reached.
This list is efficient only for short queues.
*/
template <typename TimeUnit> class DeferredQueuePerformer: public DeferredQueue<TimeUnit> {
public:
/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
DeferredQueuePerformer(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {}
/*!
Runs for @c length units of time.
The constructor-supplied target will be called with one or more periods that add up to @c length;
any scheduled actions will be called between periods.
*/
void run_for(TimeUnit length) {
auto time_to_next = DeferredQueue<TimeUnit>::time_until_next_action();
while(time_to_next != TimeUnit(-1) && time_to_next <= length) {
target_(time_to_next);
length -= time_to_next;
DeferredQueue<TimeUnit>::advance(time_to_next);
}
DeferredQueue<TimeUnit>::advance(length);
target_(length);
// TODO: optimise this to avoid the multiple std::vector deletes. Find a neat way to expose that solution, maybe?
}
private:
std::function<void(TimeUnit)> target_;
};
#endif /* DeferredQueue_h */

View File

@ -76,6 +76,48 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
bool is_flushed_ = true;
};
/*!
A RealTimeActor presents the same interface as a JustInTimeActor but doesn't defer work.
Time added will be performed immediately.
Its primary purpose is to allow consumers to remain flexible in their scheduling.
*/
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class RealTimeActor {
public:
template<typename... Args> RealTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
forceinline void operator += (const LocalTimeScale &rhs) {
if constexpr (multiplier == 1 && divider == 1) {
object_.run_for(TargetTimeScale(rhs));
return;
}
if constexpr (multiplier == 1) {
accumulated_time_ += rhs;
} else {
accumulated_time_ += rhs * multiplier;
}
if constexpr (divider == 1) {
const auto duration = accumulated_time_.template flush<TargetTimeScale>();
object_.run_for(duration);
} else {
const auto duration = accumulated_time_.template divide<TargetTimeScale>(LocalTimeScale(divider));
if(duration > TargetTimeScale(0))
object_.run_for(duration);
}
}
forceinline T *operator->() { return &object_; }
forceinline const T *operator->() const { return &object_; }
forceinline T *last_valid() { return &object_; }
forceinline void flush() {}
private:
T object_;
LocalTimeScale accumulated_time_;
};
/*!
A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
Any time the amount of accumulated time crosses a threshold provided at construction time,

View File

@ -255,7 +255,7 @@ class VideoBase {
void output_fat_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const;
// Maintain a DeferredQueue for delayed mode switches.
DeferredQueue<Cycles> deferrer_;
DeferredQueuePerformer<Cycles> deferrer_;
};
template <class BusHandler, bool is_iie> class Video: public VideoBase {

View File

@ -26,7 +26,7 @@ using namespace Apple::Macintosh;
Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator) :
audio_(audio),
drive_speed_accumulator_(drive_speed_accumulator),
crt_(704, 1, 370, Outputs::Display::ColourSpace::YIQ, 1, 1, 6, false, Outputs::Display::InputDataType::Luminance1) {
crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) {
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));

View File

@ -101,8 +101,10 @@ class ConcreteMachine:
const bool is_early_tos = true;
if(is_early_tos) {
rom_start_ = 0xfc0000;
for(c = 0xfc; c < 0xff; ++c) memory_map_[c] = BusDevice::ROM;
} else {
rom_start_ = 0xe00000;
for(c = 0xe0; c < 0xe4; ++c) memory_map_[c] = BusDevice::ROM;
}
@ -245,7 +247,7 @@ class ConcreteMachine:
case BusDevice::ROM:
memory = rom_.data();
address %= rom_.size();
address -= rom_start_;
break;
case BusDevice::Floating:
@ -469,6 +471,7 @@ class ConcreteMachine:
length -= cycles_until_video_event_;
video_ += cycles_until_video_event_;
cycles_until_video_event_ = video_->get_next_sequence_point();
assert(cycles_until_video_event_ > HalfCycles(0));
mfp_->set_timer_event_input(1, video_->display_enabled());
update_interrupt_input();
@ -504,6 +507,7 @@ class ConcreteMachine:
std::vector<uint8_t> ram_;
std::vector<uint8_t> rom_;
uint32_t rom_start_ = 0;
enum class BusDevice {
/// A mostly RAM page is one that returns ROM for the first 8 bytes, RAM elsewhere.
@ -567,7 +571,7 @@ class ConcreteMachine:
GPIP 0: centronics busy
*/
mfp_->set_port_input(
0x80 | // b7: Monochrome monitor detect (1 = is monochrome).
0x80 | // b7: Monochrome monitor detect (0 = is monochrome).
0x40 | // b6: RS-232 ring indicator.
(dma_->get_interrupt_line() ? 0x00 : 0x20) | // b5: FD/HS interrupt (0 = interrupt requested).
((keyboard_acia_->get_interrupt_line() || midi_acia_->get_interrupt_line()) ? 0x00 : 0x10) | // b4: Keyboard/MIDI interrupt (0 = interrupt requested).

View File

@ -13,6 +13,8 @@
#include <algorithm>
#include <cstring>
#define CYCLE(x) ((x) * 2)
using namespace Atari::ST;
namespace {
@ -29,7 +31,7 @@ const struct VerticalParams {
} vertical_params[3] = {
{63, 263, 313}, // 47 rather than 63 on early machines.
{34, 234, 263},
{1, 401, 500} // 72 Hz mode: who knows?
{34, 434, 500} // Guesswork: (i) nobody ever recommends 72Hz mode for opening the top border, so it's likely to be the same as another mode; (ii) being the same as PAL feels too late.
};
/// @returns The correct @c VerticalParams for output at @c frequency.
@ -37,8 +39,6 @@ const VerticalParams &vertical_parameters(Video::FieldFrequency frequency) {
return vertical_params[int(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
@ -57,13 +57,23 @@ const struct HorizontalParams {
const int set_blank;
const int reset_blank;
const int length;
const int vertical_decision;
LineLength length;
} horizontal_params[3] = {
{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.
{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)];
}
@ -79,10 +89,10 @@ struct Checker {
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);
assert(horizontal.set_blank+50 < horizontal.length.length);
} else {
assert(horizontal.set_enable < horizontal.reset_enable);
assert(horizontal.set_enable+50 <horizontal.length);
assert(horizontal.set_enable+50 <horizontal.length.length);
}
// Expected vertical order of events: reset blank, enable display, disable display, enable blank (at least 50 before end of line), end of line
@ -97,12 +107,13 @@ struct Checker {
const int de_delay_period = CYCLE(28); // Amount of time after DE that observed DE changes. NB: HACK HERE. This currently incorporates the MFP recognition delay. MUST FIX.
const int vsync_x_position = CYCLE(56); // 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.
const int line_length_latch_position = CYCLE(54);
const int hsync_delay_period = hsync_end; // Signal hsync at the end of the line.
const int hsync_delay_period = CYCLE(8); // Signal hsync at the end of the line.
const int vsync_delay_period = hsync_delay_period; // Signal vsync with the same delay as hsync.
const int load_delay_period = CYCLE(4); // Amount of time after DE that observed DE changes. NB: HACK HERE. This currently incorporates the MFP recognition delay. MUST FIX.
// "VSYNC starts 104 cycles after the start of the previous line's HSYNC, so that's 4 cycles before DE would be activated. ";
// that's an inconsistent statement since it would imply VSYNC at +54, which is 2 cycles before DE in 60Hz mode and 6 before
// in 50Hz mode. I've gone with 56, to be four cycles ahead of DE in 50Hz mode.
@ -110,13 +121,13 @@ const int vsync_delay_period = hsync_delay_period; // Signal vsync with the same
}
Video::Video() :
deferrer_([=] (HalfCycles duration) { advance(duration); }),
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red4Green4Blue4),
crt_(2048, 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red4Green4Blue4),
// crt_(896, 1, 500, 5, Outputs::Display::InputDataType::Red4Green4Blue4),
video_stream_(crt_, palette_) {
// Show a total of 260 lines; a little short for PAL but a compromise between that and the ST's
// usual output height of 200 lines.
crt_.set_visible_area(crt_.get_rect_for_area(33, 260, 220, 850, 4.0f / 3.0f));
crt_.set_visible_area(crt_.get_rect_for_area(33, 260, 440, 1700, 4.0f / 3.0f));
}
void Video::set_ram(uint16_t *ram, size_t size) {
@ -128,7 +139,7 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
}
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
return crt_.get_scaled_scan_status() / 2.0f;
return crt_.get_scaled_scan_status() / 4.0f;
}
void Video::set_display_type(Outputs::Display::DisplayType display_type) {
@ -136,47 +147,32 @@ void Video::set_display_type(Outputs::Display::DisplayType display_type) {
}
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());
// Effect any changes in visible state out here; they're not relevant in the inner loop.
if(!pending_events_.empty()) {
auto erase_iterator = pending_events_.begin();
int duration_remaining = integer_duration;
while(erase_iterator != pending_events_.end()) {
erase_iterator->delay -= duration_remaining;
if(erase_iterator->delay <= 0) {
duration_remaining = -erase_iterator->delay;
erase_iterator->apply(public_state_);
++erase_iterator;
} else {
break;
}
}
if(erase_iterator != pending_events_.begin()) {
pending_events_.erase(pending_events_.begin(), erase_iterator);
}
}
assert(integer_duration >= 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_;
int next_event = line_length_.length;
const int next_deferred_event = deferrer_.time_until_next_action().as<int>();
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);
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_ - 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);
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) {
@ -189,6 +185,8 @@ void Video::advance(HalfCycles duration) {
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_;
@ -214,13 +212,16 @@ void Video::advance(HalfCycles duration) {
} else if(!load_) {
video_stream_.output(run_length, VideoStream::OutputMode::Pixels);
} else {
const int since_load = x_ - load_base_;
const int start = x_ - load_base_;
const int end = start + run_length;
// 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;
int start_column = start >> 3;
const int end_column = end >> 3;
const int start_offset = start & 7;
const int end_offset = end & 7;
// Rules obeyed below:
//
@ -229,35 +230,40 @@ void Video::advance(HalfCycles duration) {
// was reloaded by the fetch depends on the FIFO.
if(start_column == end_column) {
if(!start_offset) {
push_latched_data();
}
video_stream_.output(run_length, VideoStream::OutputMode::Pixels);
} else {
// Continue the current column if partway across.
if(since_load&7) {
if(start_offset) {
// If at least one column boundary is crossed, complete this column.
video_stream_.output(8 - (since_load & 7), VideoStream::OutputMode::Pixels);
video_stream_.output(8 - start_offset, 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();
video_stream_.output(8, VideoStream::OutputMode::Pixels);
}
// 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);
if(end_offset) {
push_latched_data();
video_stream_.output(end_offset, VideoStream::OutputMode::Pixels);
}
}
}
// Check for whether line length should have been latched during this run.
if(x_ <= CYCLE(54) && (x_ + run_length) > CYCLE(54)) line_length_ = horizontal_timings.length;
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 cycle 502.
if(x_ <= CYCLE(502) && (x_ + run_length) > CYCLE(502)) {
// 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;
@ -282,23 +288,17 @@ void Video::advance(HalfCycles duration) {
// 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;
// next_load_toggle_ is less predictable; test separately because it may coincide
// with one of the above tests.
if(next_load_toggle_ == x_) {
next_load_toggle_ = -1;
load_ ^= true;
load_base_ = x_;
}
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) {
@ -310,7 +310,7 @@ void Video::advance(HalfCycles duration) {
// Check whether the terminating event was end-of-line; if so then advance
// the vertical bits of state.
if(x_ == line_length_) {
if(x_ == line_length_.length) {
x_ = 0;
vertical_ = next_vertical_;
y_ = next_y_;
@ -333,21 +333,30 @@ void Video::advance(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 load line.
deferrer_.defer(load_delay_period, [this, next_display_enable] {
this->load_ = next_display_enable;
this->load_base_ = this->x_;
});
// Schedule change in inwardly-visible effect.
next_load_toggle_ = x_ + 8; // 4 cycles = 8 half-cycles
// 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.
add_event(hsync_delay_period - integer_duration, horizontal_.sync ? Event::Type::SetHsync : Event::Type::ResetHsync);
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.
add_event(vsync_delay_period - integer_duration, vertical_.sync ? Event::Type::SetVsync : Event::Type::ResetVsync);
deferrer_.defer(vsync_delay_period, [this, next_vertical_sync = vertical_.sync] {
this->public_state_.vsync = next_vertical_sync;
});
}
}
}
@ -400,11 +409,12 @@ HalfCycles Video::get_next_sequence_point() {
const auto horizontal_timings = horizontal_parameters(field_frequency_);
int event_time = line_length_; // Worst case: report end of line.
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.
if(!pending_events_.empty()) {
event_time = std::min(event_time, x_ + pending_events_.front().delay);
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<int>());
}
// If this is a vertically-enabled line, check for the display enable boundaries, + the standard delay.
@ -423,11 +433,17 @@ HalfCycles Video::get_next_sequence_point() {
}
// 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_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);
}
/* Hereby assumed: hsync end will be communicated at end of line: */
static_assert(hsync_end == hsync_delay_period);
// 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_);
@ -559,12 +575,12 @@ void Video::VideoStream::generate(int duration, OutputMode mode, bool is_termina
if(mode != OutputMode::Pixels) {
switch(mode) {
default:
case OutputMode::Sync: crt_.output_sync(duration_); break;
case OutputMode::Blank: crt_.output_blank(duration_); break;
case OutputMode::ColourBurst: crt_.output_default_colour_burst(duration_); break;
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
// Reseed duration.
duration_ = duration;
// The shifter should keep running, so throw away the proper amount of content.
@ -614,7 +630,7 @@ 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<uint16_t *>(crt_.begin_data(1));
if(colour_pointer) *colour_pointer = (bpp_ != OutputBpp::One) ? palette_[0] : 0;
crt_.output_level(duration_);
crt_.output_level(duration_*2);
duration_ = 0;
}
@ -715,7 +731,7 @@ void Video::VideoStream::output_pixels(int duration) {
}
// Check whether the limit has been reached.
if(pixel_pointer_ == allocation_size) {
if(pixel_pointer_ >= allocation_size - 32) {
flush_pixels();
}
}
@ -725,12 +741,12 @@ void Video::VideoStream::output_pixels(int duration) {
if(pixels) {
int leftover_duration = pixels;
switch(bpp_) {
case OutputBpp::One: leftover_duration >>= 1; break;
default: break;
case OutputBpp::Four: leftover_duration <<= 1; break;
default: leftover_duration >>= 1; break;
case OutputBpp::Two: break;
case OutputBpp::Four: leftover_duration <<= 1; break;
}
shift(leftover_duration);
crt_.output_data(leftover_duration);
crt_.output_data(leftover_duration*2);
}
}
@ -738,9 +754,9 @@ 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_ >> 1, size_t(pixel_pointer_)); break;
default: crt_.output_data(pixel_pointer_); break;
case OutputBpp::Four: crt_.output_data(pixel_pointer_ << 1, size_t(pixel_pointer_)); break;
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;
}
}
@ -761,7 +777,13 @@ void Video::VideoStream::set_bpp(OutputBpp bpp) {
}
void Video::VideoStream::load(uint64_t value) {
output_shifter_ = 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.

View File

@ -21,6 +21,12 @@ class VideoTester;
namespace Atari {
namespace ST {
struct LineLength {
int length = 1024;
int hsync_start = 1024;
int hsync_end = 1024;
};
/*!
Models a combination of the parts of the GLUE, MMU and Shifter that in net
form the video subsystem of the Atari ST. So not accurate to a real chip, but
@ -115,7 +121,6 @@ class Video {
Range get_memory_access_range();
private:
void advance(HalfCycles duration);
DeferredQueue<HalfCycles> deferrer_;
Outputs::CRT::CRT crt_;
@ -131,7 +136,6 @@ 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;
@ -163,7 +167,7 @@ class Video {
} sync_schedule = SyncSchedule::None;
bool sync = false;
} vertical_, next_vertical_;
int line_length_ = 1024;
LineLength line_length_;
int data_latch_position_ = 0;
int data_latch_read_position_ = 0;
@ -237,59 +241,6 @@ class Video {
bool vsync = false;
} public_state_;
struct Event {
int delay;
enum class Type {
SetDisplayEnable, ResetDisplayEnable,
SetHsync, ResetHsync,
SetVsync, ResetVsync,
} type;
Event(Type type, int delay) : delay(delay), type(type) {}
void apply(PublicState &state) {
apply(type, state);
}
static void apply(Type type, PublicState &state) {
switch(type) {
default:
case Type::SetDisplayEnable: state.display_enable = true; break;
case Type::ResetDisplayEnable: state.display_enable = false; break;
case Type::SetHsync: state.hsync = true; break;
case Type::ResetHsync: state.hsync = false; break;
case Type::SetVsync: state.vsync = true; break;
case Type::ResetVsync: state.vsync = false; break;
}
}
};
std::vector<Event> pending_events_;
void add_event(int delay, Event::Type type) {
// Apply immediately if there's no delay (or a negative delay).
if(delay <= 0) {
Event::apply(type, public_state_);
return;
}
if(!pending_events_.empty()) {
// Otherwise enqueue, having subtracted the delay for any preceding events,
// and subtracting from the subsequent, if any.
auto insertion_point = pending_events_.begin();
while(insertion_point != pending_events_.end() && insertion_point->delay < delay) {
delay -= insertion_point->delay;
++insertion_point;
}
if(insertion_point != pending_events_.end()) {
insertion_point->delay -= delay;
}
pending_events_.emplace(insertion_point, type, delay);
} else {
pending_events_.emplace_back(type, delay);
}
}
friend class ::VideoTester;
};

View File

@ -160,6 +160,7 @@
4B2B3A4C1F9B8FA70062DABF /* MemoryFuzzer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A481F9B8FA70062DABF /* MemoryFuzzer.cpp */; };
4B2BF19123DCC6A200C3AD60 /* BD500.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7BA03523CEB86000B98D9E /* BD500.cpp */; };
4B2BF19223DCC6A800C3AD60 /* STX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7BA03323C58B1E00B98D9E /* STX.cpp */; };
4B2BF19623E10F0100C3AD60 /* CSHighPrecisionTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BF19523E10F0000C3AD60 /* CSHighPrecisionTimer.m */; };
4B2BFC5F1D613E0200BA3AA9 /* TapePRG.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFC5D1D613E0200BA3AA9 /* TapePRG.cpp */; };
4B2BFDB21DAEF5FF001A68B8 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFDB01DAEF5FF001A68B8 /* Video.cpp */; };
4B2C45421E3C3896002A2389 /* cartridge.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B2C45411E3C3896002A2389 /* cartridge.png */; };
@ -1002,6 +1003,8 @@
4B2B3A481F9B8FA70062DABF /* MemoryFuzzer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MemoryFuzzer.cpp; sourceTree = "<group>"; };
4B2B3A491F9B8FA70062DABF /* MemoryFuzzer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MemoryFuzzer.hpp; sourceTree = "<group>"; };
4B2B3A4A1F9B8FA70062DABF /* Typer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Typer.hpp; sourceTree = "<group>"; };
4B2BF19423E10F0000C3AD60 /* CSHighPrecisionTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSHighPrecisionTimer.h; sourceTree = "<group>"; };
4B2BF19523E10F0000C3AD60 /* CSHighPrecisionTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSHighPrecisionTimer.m; sourceTree = "<group>"; };
4B2BFC5D1D613E0200BA3AA9 /* TapePRG.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TapePRG.cpp; sourceTree = "<group>"; };
4B2BFC5E1D613E0200BA3AA9 /* TapePRG.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TapePRG.hpp; sourceTree = "<group>"; };
4B2BFDB01DAEF5FF001A68B8 /* Video.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Video.cpp; path = Oric/Video.cpp; sourceTree = "<group>"; };
@ -2063,6 +2066,15 @@
path = Utility;
sourceTree = "<group>";
};
4B2BF19323E10F0000C3AD60 /* High Precision Timer */ = {
isa = PBXGroup;
children = (
4B2BF19423E10F0000C3AD60 /* CSHighPrecisionTimer.h */,
4B2BF19523E10F0000C3AD60 /* CSHighPrecisionTimer.m */,
);
path = "High Precision Timer";
sourceTree = "<group>";
};
4B2E2D9E1C3A070900138695 /* Electron */ = {
isa = PBXGroup;
children = (
@ -3257,6 +3269,7 @@
4B2A538F1D117D36003C6002 /* Audio */,
4B643F3D1D77B88000D431D6 /* Document Controller */,
4B55CE551C3B7D360093A61B /* Documents */,
4B2BF19323E10F0000C3AD60 /* High Precision Timer */,
4BBFE83B21015D9C00BF1C40 /* Joystick Manager */,
4B2A53921D117D36003C6002 /* Machine */,
4B55DD7F20DF06680043F2E5 /* MachinePicker */,
@ -3839,17 +3852,19 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0700;
LastUpgradeCheck = 1100;
LastUpgradeCheck = 1130;
ORGANIZATIONNAME = "Thomas Harte";
TargetAttributes = {
4B055A691FAE763F0060FFFF = {
CreatedOnToolsVersion = 9.1;
DevelopmentTeam = CP2SKEB3XT;
ProvisioningStyle = Automatic;
};
4BB73E9D1B587A5100552FC2 = {
CreatedOnToolsVersion = 7.0;
DevelopmentTeam = CP2SKEB3XT;
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
@ -3858,11 +3873,14 @@
};
4BB73EB11B587A5100552FC2 = {
CreatedOnToolsVersion = 7.0;
DevelopmentTeam = CP2SKEB3XT;
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
TestTargetID = 4BB73E9D1B587A5100552FC2;
};
4BB73EBC1B587A5100552FC2 = {
CreatedOnToolsVersion = 7.0;
DevelopmentTeam = CP2SKEB3XT;
LastSwiftMigration = 1020;
TestTargetID = 4BB73E9D1B587A5100552FC2;
};
@ -4442,6 +4460,7 @@
4B54C0CB1F8D92590050900F /* Keyboard.cpp in Sources */,
4BEA525E1DF33323007E74F2 /* Tape.cpp in Sources */,
4B07835A1FC11D10001D12BB /* Configurable.cpp in Sources */,
4B2BF19623E10F0100C3AD60 /* CSHighPrecisionTimer.m in Sources */,
4B8334951F5E25B60097E338 /* C1540.cpp in Sources */,
4B89453C201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
4B595FAD2086DFBA0083CAA8 /* AudioToggle.cpp in Sources */,
@ -4911,6 +4930,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = CP2SKEB3XT;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(USER_LIBRARY_DIR)/Frameworks",
@ -4931,6 +4951,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = CP2SKEB3XT;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(USER_LIBRARY_DIR)/Frameworks",
@ -5059,9 +5080,10 @@
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
CODE_SIGN_ENTITLEMENTS = "Clock Signal/Clock Signal.entitlements";
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = CP2SKEB3XT;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -5086,6 +5108,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "TH.Clock-Signal";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Clock Signal/ClockSignal-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -5104,9 +5127,10 @@
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
CODE_SIGN_ENTITLEMENTS = "Clock Signal/Clock Signal.entitlements";
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = CP2SKEB3XT;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -5133,6 +5157,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "TH.Clock-Signal";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Clock Signal/ClockSignal-Bridging-Header.h";
SWIFT_VERSION = 5.0;
};
@ -5144,11 +5169,17 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Clock Signal/Clock Signal.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO;
INFOPLIST_FILE = "Clock SignalTests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "TH.Clock-SignalTests";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Clock SignalTests/Bridges/Clock SignalTests-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -5162,12 +5193,18 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Clock Signal/Clock Signal.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO;
GCC_OPTIMIZATION_LEVEL = 2;
INFOPLIST_FILE = "Clock SignalTests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "TH.Clock-SignalTests";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Clock SignalTests/Bridges/Clock SignalTests-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Clock Signal.app/Contents/MacOS/Clock Signal";
@ -5178,6 +5215,7 @@
isa = XCBuildConfiguration;
buildSettings = {
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = CP2SKEB3XT;
INFOPLIST_FILE = "Clock SignalUITests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "TH.Clock-SignalUITests";
@ -5192,6 +5230,7 @@
isa = XCBuildConfiguration;
buildSettings = {
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = CP2SKEB3XT;
INFOPLIST_FILE = "Clock SignalUITests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "TH.Clock-SignalUITests";

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4B055A691FAE763F0060FFFF"
BuildableName = "Clock Signal Kiosk"
BlueprintName = "Clock Signal Kiosk"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
disableMainThreadChecker = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4B055A691FAE763F0060FFFF"
BuildableName = "Clock Signal Kiosk"
BlueprintName = "Clock Signal Kiosk"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "/Users/thomasharte/Downloads/test-dsk-for-rw-and-50-60-hz/TEST-RW-60Hz.DSK"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--speed=5"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--rompath=/Users/thomasharte/Projects/CLK/ROMImages"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4B055A691FAE763F0060FFFF"
BuildableName = "Clock Signal Kiosk"
BlueprintName = "Clock Signal Kiosk"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1100"
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4BB73EB11B587A5100552FC2"
BuildableName = "Clock SignalTests.xctest"
BlueprintName = "Clock SignalTests"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4BB73E9D1B587A5100552FC2"
BuildableName = "Clock Signal.app"
BlueprintName = "Clock Signal"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4BB73E9D1B587A5100552FC2"
BuildableName = "Clock Signal.app"
BlueprintName = "Clock Signal"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -19,6 +19,9 @@
/// Initialises a new instance of the high precision timer; the timer will begin
/// ticking immediately.
///
/// @param task The block to perform each time the timer fires.
/// @param interval The interval at which to fire the timer, in nanoseconds.
- (instancetype)initWithTask:(dispatch_block_t)task interval:(uint64_t)interval;
/// Stops the timer.

View File

@ -95,18 +95,29 @@ struct VideoTester {
// MARK: - Sequence Point Prediction Tests
/// Tests that no events occur outside of the sequence points the video predicts.
- (void)testSequencePoints {
- (void)testSequencePoints50 {
// Set 4bpp, 50Hz.
_video->write(0x05, 0x0200);
_video->write(0x30, 0x0000);
// Run for [more than] a whole frame making sure that no observeable outputs
[self runSequencePointsTest];
}
- (void)testSequencePoints72 {
// Set 1bpp, 72Hz.
_video->write(0x30, 0x0200);
[self runSequencePointsTest];
}
- (void)runSequencePointsTest {
// Run for [more than] two frames making sure that no observeable outputs
// change at any time other than a sequence point.
HalfCycles next_event;
bool display_enable = false;
bool vsync = false;
bool hsync = false;
for(size_t c = 0; c < 10 * 1000 * 1000; ++c) {
for(size_t c = 0; c < 8000000 / 20; ++c) {
const bool is_transition_point = next_event == HalfCycles(0);
if(is_transition_point) {
@ -322,6 +333,16 @@ struct RunLength {
XCTAssertNotEqual([self currentVideoAddress], 0);
}
// MARK: - Tests Relating To Specific Bugs
- (void)test72LineLength {
// Set 1bpp, 72Hz.
_video->write(0x30, 0x0200);
[self syncToStartOfLine];
_video->run_for(HalfCycles(400)); // 392, 399, 406
}
// MARK: - Tests Correlating To Exact Pieces of Software
- (void)testUnionDemoScroller {

View File

@ -161,6 +161,18 @@ CRT::CRT( int cycles_per_line,
set_new_display_type(cycles_per_line, display_type);
}
CRT::CRT(int cycles_per_line,
int clocks_per_pixel_greatest_common_divisor,
int height_of_display,
int vertical_sync_half_lines,
Outputs::Display::InputDataType data_type) {
scan_target_modals_.input_data_type = data_type;
scan_target_modals_.cycles_per_line = cycles_per_line;
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
set_new_timing(cycles_per_line, height_of_display, Outputs::Display::ColourSpace::YIQ, 1, 1, vertical_sync_half_lines, false);
}
// MARK: - Sync loop
Flywheel::SyncEvent CRT::get_next_vertical_sync_event(bool vsync_is_requested, int cycles_to_run_for, int *cycles_advanced) {
@ -294,6 +306,8 @@ void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_
// MARK: - stream feeding methods
void CRT::output_scan(const Scan *const scan) {
assert(scan->number_of_cycles >= 0);
// Simplified colour burst logic: if it's within the back porch we'll take it.
if(scan->type == Scan::Type::ColourBurst) {
if(!colour_burst_amplitude_ && horizontal_flywheel_->get_current_time() < (horizontal_flywheel_->get_standard_period() * 12) >> 6) {

View File

@ -123,6 +123,15 @@ class CRT {
bool should_alternate,
Outputs::Display::InputDataType data_type);
/*! Constructs a monitor-style CRT — one that will take only an RGB or monochrome signal, and therefore has
no colour space or colour subcarrier frequency. This monitor will automatically map colour bursts to the black level.
*/
CRT(int cycles_per_line,
int clocks_per_pixel_greatest_common_divisor,
int height_of_display,
int vertical_sync_half_lines,
Outputs::Display::InputDataType data_type);
/*! Exactly identical to calling the designated constructor with colour subcarrier information
looked up by display type.
*/
@ -227,10 +236,15 @@ class CRT {
@returns A pointer to the allocated area if room is available; @c nullptr otherwise.
*/
inline uint8_t *begin_data(std::size_t required_length, std::size_t required_alignment = 1) {
const auto result = scan_target_->begin_data(required_length, required_alignment);
#ifndef NDEBUG
allocated_data_length_ = required_length;
// If data was allocated, make a record of how much so as to be able to hold the caller to that
// contract later. If allocation failed, don't constrain the caller. This allows callers that
// allocate on demand but may allow one failure to hold for a longer period — e.g. until the
// next line.
allocated_data_length_ = result ? required_length : std::numeric_limits<size_t>::max();
#endif
return scan_target_->begin_data(required_length, required_alignment);
return result;
}
/*! Sets the gamma exponent for the simulated screen. */

View File

@ -106,7 +106,7 @@ template <typename BitHandler, size_t length_of_history = 3> class DigitalPhaseL
// In net: use an unweighted average of the stored offsets to compute current window size,
// bucketing them by rounding to the nearest multiple of the base clocks per bit
window_length_ = total_spacing_ / total_divisor_;
window_length_ = std::max(total_spacing_ / total_divisor_, Cycles::IntType(1));
// Also apply a difference to phase, use a simple spring mechanism as a lowpass filter.
const auto error = new_phase - (window_length_ >> 1);