// // Flywheel.hpp // Clock Signal // // Created by Thomas Harte on 11/02/2016. // Copyright 2016 Thomas Harte. All rights reserved. // #pragma once #include #include #include #include #include #include namespace Outputs::CRT { /*! Provides timing for a two-phase signal consisting of a retrace phase followed by a scan phase, announcing the start and end of retrace and providing the abiliy to read the current scanning position. The @c Flywheel will attempt to converge with timing implied by synchronisation pulses. */ struct Flywheel { /*! Constructs an instance of @c Flywheel. @param standard_period The expected amount of time between one synchronisation and the next. @param retrace_time The amount of time it takes to complete a retrace. @param sync_error_window The permitted deviation of sync timings from the norm. */ Flywheel(const int standard_period, const int retrace_time, const int sync_error_window) noexcept : standard_period_(standard_period), retrace_time_(retrace_time), sync_error_window_(sync_error_window), counter_before_retrace_(standard_period - retrace_time), expected_next_sync_(standard_period) {} Flywheel() = default; enum SyncEvent { None, StartRetrace, EndRetrace }; /*! @param sync_is_requested @c true indicates that the flywheel should act as though having received a synchronisation request now; @c false indicates no such event was detected. @param cycles_to_run_for The maximum number of cycles to look ahead. @returns A pair of the next synchronisation event and number of cycles until it occurs. */ std::pair next_event_in_period( const bool sync_is_requested, const int cycles_to_run_for ) { // Calculates the next expected value for an event given the current expectation and the actual value. // // In practice this is a weighted mix of the two values, with the exact weighting affecting how // quickly the flywheel adjusts to new input. It's a IIR lowpass filter. constexpr auto mix = [](const int expected, const int actual) { return (expected + actual) >> 1; }; // A debugging helper. constexpr auto require_positive = [](const int value) { assert(value >= 0); return value; }; // If sync is signalled _now_, consider adjusting expected_next_sync_. if(sync_is_requested) { const auto last_sync = expected_next_sync_; if(counter_ < sync_error_window_ || counter_ > expected_next_sync_ - sync_error_window_) { const int time_now = (counter_ < sync_error_window_) ? expected_next_sync_ + counter_ : counter_; expected_next_sync_ = mix(expected_next_sync_, time_now); } else { ++number_of_surprises_; if(counter_ < retrace_time_ + (expected_next_sync_ >> 1)) { expected_next_sync_ = mix(expected_next_sync_, standard_period_ + sync_error_window_); } else { expected_next_sync_ = mix(expected_next_sync_, standard_period_ - sync_error_window_); } } last_adjustment_ = expected_next_sync_ - last_sync; } SyncEvent proposed_event = SyncEvent::None; int proposed_sync_time = cycles_to_run_for; // End an ongoing retrace? if(counter_ < retrace_time_ && counter_ + proposed_sync_time >= retrace_time_) { proposed_sync_time = require_positive(retrace_time_ - counter_); proposed_event = SyncEvent::EndRetrace; } // Start a retrace? if(counter_ + proposed_sync_time >= expected_next_sync_) { // A change in expectations above may have moved the expected sync time to before now. // If so, just start sync ASAP. proposed_sync_time = std::max(0, expected_next_sync_ - counter_); proposed_event = SyncEvent::StartRetrace; } return std::make_pair(proposed_event, proposed_sync_time); } bool was_stable() const { return std::abs(last_adjustment_) < (sync_error_window_ / 250); } /*! Advances a nominated amount of time, applying a previously returned synchronisation event at the end of that period. @param cycles_advanced The amount of time to run for. @param event The synchronisation event to apply after that period. */ void apply_event(const int cycles_advanced, const SyncEvent event) { // In debug builds, perform a sanity check for counter overflow. #ifndef NDEBUG const int old_counter = counter_; #endif counter_ += cycles_advanced; assert(old_counter <= counter_); switch(event) { default: return; case StartRetrace: counter_before_retrace_ = counter_ - retrace_time_; counter_ = 0; return; } } /*! Returns the current output position; while in retrace this will go down towards 0, while in scan it will go upward. @returns The current output position. */ int current_output_position() const { if(counter_ < retrace_time_) { const int retrace_distance = int((int64_t(counter_) * int64_t(standard_period_)) / int64_t(retrace_time_)); if(retrace_distance > counter_before_retrace_) return 0; return counter_before_retrace_ - retrace_distance; } return counter_ - retrace_time_; } /*! Returns the current 'phase' — 0 is the start of the display; a count up to 0 from a negative number represents the retrace period and it will then count up to @c locked_scan_period(). @returns The current output position. */ int current_phase() const { return counter_ - retrace_time_; } /*! @returns the amount of time since retrace last began. Time then counts monotonically up from zero. */ int current_time() const { return counter_; } /*! @returns whether the output is currently retracing. */ bool is_in_retrace() const { return counter_ < retrace_time_; } /*! @returns the expected length of the scan period (excluding retrace). */ int scan_period() const { return standard_period_ - retrace_time_; } /*! @returns the actual length of the scan period (excluding retrace). */ int locked_scan_period() const { return expected_next_sync_ - retrace_time_; } /*! @returns the expected length of a complete scan and retrace cycle. */ int standard_period() const { return standard_period_; } /*! @returns the actual current period for a complete scan (including retrace). */ int locked_period() const { return expected_next_sync_; } /*! @returns the amount by which the @c locked_period was adjusted, the last time that an adjustment was applied. */ int last_period_adjustment() const { return last_adjustment_; } /*! @returns the number of synchronisation events that have seemed surprising since the last time this method was called; a low number indicates good synchronisation. */ int get_and_reset_number_of_surprises() { const int result = number_of_surprises_; number_of_surprises_ = 0; return result; } /*! @returns A count of the number of retraces so far performed. */ int number_of_retraces() const { return number_of_retraces_; } /*! @returns The amount of time this flywheel spends in retrace, as supplied at construction. */ int retrace_period() const { return retrace_time_; } /*! @returns `true` if a sync is expected soon or if the time at which it was expected (or received) was recent. */ bool is_near_expected_sync() const { return (counter_ < (standard_period_ / 100)) || (counter_ >= expected_next_sync_ - (standard_period_ / 100)); } private: int standard_period_; // Idealised length of time between syncs. int retrace_time_; // Amount of time it takes to perform a retrace. int sync_error_window_; // The window either side of the next expected sync in which other syncs are accepted. int counter_ = 0; // Time since the _start_ of the last sync. int counter_before_retrace_; // Value of counter_ immediately before retrace began. int expected_next_sync_; // Current expection of when the next sync will occur (implying velocity). int number_of_surprises_ = 0; // Count of surprising syncs. int number_of_retraces_ = 0; // Numer of retraces to date. int last_adjustment_ = 0; // The amount by which expected_next_sync_ was adjusted at the last sync. /* Implementation notes: Retrace takes a fixed amount of time and runs during [0, _retrace_time). For the current line, scan then occurs from [_retrace_time, _expected_next_sync), at which point retrace begins and the internal counter is reset. All synchronisation events that occur within (-_sync_error_window, _sync_error_window) of the expected synchronisation time will cause a proportional adjustment in the expected time for the next synchronisation. Other synchronisation events are clamped as though they occurred in that range. */ }; }