mirror of
https://github.com/TomHarte/CLK.git
synced 2024-11-30 04:50:08 +00:00
86 lines
3.1 KiB
C++
86 lines
3.1 KiB
C++
//
|
|
// ScanSynchroniser.hpp
|
|
// Clock Signal
|
|
//
|
|
// Created by Thomas Harte on 09/02/2020.
|
|
// Copyright © 2020 Thomas Harte. All rights reserved.
|
|
//
|
|
|
|
#pragma once
|
|
|
|
#include "../Outputs/ScanTarget.hpp"
|
|
|
|
#include <cmath>
|
|
|
|
namespace Time {
|
|
|
|
/*!
|
|
Where an emulated machine is sufficiently close to a host machine's frame rate that a small nudge in
|
|
its speed multiplier will bring it into frame synchronisation, the ScanSynchroniser provides a sequence of
|
|
speed multipliers designed both to adjust the machine to the proper speed and, in a reasonable amount
|
|
of time, to bring it into phase.
|
|
*/
|
|
class ScanSynchroniser {
|
|
public:
|
|
/*!
|
|
@returns @c true if the emulated machine can be synchronised with the host frame output based on its
|
|
current @c [scan]status and the host machine's @c frame_duration; @c false otherwise.
|
|
*/
|
|
bool can_synchronise(const Outputs::Display::ScanStatus &scan_status, double frame_duration) {
|
|
ratio_ = 1.0;
|
|
if(scan_status.field_duration_gradient < 0.00001) {
|
|
// Check out the machine's current frame time.
|
|
// If it's within 3% of a non-zero integer multiple of the
|
|
// display rate, mark this time window to be split over the sync.
|
|
ratio_ = (frame_duration * base_multiplier_) / scan_status.field_duration;
|
|
const double integer_ratio = round(ratio_);
|
|
if(integer_ratio > 0.0) {
|
|
ratio_ /= integer_ratio;
|
|
return ratio_ <= maximum_rate_adjustment && ratio_ >= 1.0 / maximum_rate_adjustment;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*!
|
|
@returns The appropriate speed multiplier for the next frame based on the inputs previously supplied to @c can_synchronise.
|
|
Results are undefined if @c can_synchroise returned @c false.
|
|
*/
|
|
double next_speed_multiplier(const Outputs::Display::ScanStatus &scan_status) {
|
|
// The host versus emulated ratio is calculated based on the current perceived frame duration of the machine.
|
|
// Either that number is exactly correct or it's already the result of some sort of low-pass filter. So there's
|
|
// no benefit to second guessing it here — just take it to be correct.
|
|
//
|
|
// ... with one slight caveat, which is that it is desireable to adjust phase here, to align vertical sync points.
|
|
// So the set speed multiplier may be adjusted slightly to aim for that.
|
|
double speed_multiplier = 1.0 / (ratio_ / base_multiplier_);
|
|
if(scan_status.current_position > 0.0) {
|
|
if(scan_status.current_position < 0.5) speed_multiplier /= phase_adjustment_ratio;
|
|
else speed_multiplier *= phase_adjustment_ratio;
|
|
}
|
|
speed_multiplier_ = (speed_multiplier_ * 0.95) + (speed_multiplier * 0.05);
|
|
return speed_multiplier_ * base_multiplier_;
|
|
}
|
|
|
|
void set_base_speed_multiplier(double multiplier) {
|
|
base_multiplier_ = multiplier;
|
|
}
|
|
|
|
double get_base_speed_multiplier() {
|
|
return base_multiplier_;
|
|
}
|
|
|
|
private:
|
|
static constexpr double maximum_rate_adjustment = 1.03;
|
|
static constexpr double phase_adjustment_ratio = 1.005;
|
|
|
|
// Managed local state.
|
|
double speed_multiplier_ = 1.0;
|
|
double base_multiplier_ = 1.0;
|
|
|
|
// Temporary storage to bridge the can_synchronise -> next_speed_multiplier gap.
|
|
double ratio_ = 1.0;
|
|
};
|
|
|
|
}
|