mirror of
https://github.com/TomHarte/CLK.git
synced 2025-02-16 18:30:32 +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:
commit
0310f94f0c
@ -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 */
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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));
|
||||
|
@ -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).
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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>
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1100"
|
||||
LastUpgradeVersion = "1130"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -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>
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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. */
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user