mirror of
https://github.com/TomHarte/CLK.git
synced 2025-04-09 15:39:08 +00:00
Merge pull request #737 from TomHarte/Multisync
Adds multisync monitor support to the Oric.
This commit is contained in:
commit
2103e1b470
@ -60,6 +60,13 @@ void MultiCRTMachine::set_scan_target(Outputs::Display::ScanTarget *scan_target)
|
||||
if(crt_machine) crt_machine->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus MultiCRTMachine::get_scan_status() const {
|
||||
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
|
||||
if(crt_machine) crt_machine->get_scan_status();
|
||||
|
||||
return Outputs::Display::ScanStatus();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() {
|
||||
return speaker_;
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ class MultiCRTMachine: public CRTMachine::Machine {
|
||||
|
||||
// Below is the standard CRTMachine::Machine interface; see there for documentation.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override;
|
||||
Outputs::Display::ScanStatus get_scan_status() const override;
|
||||
Outputs::Speaker::Speaker *get_speaker() override;
|
||||
void run_for(Time::Seconds duration) override;
|
||||
|
||||
|
@ -43,6 +43,13 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Acts exactly as per the standard ->, but preserves constness.
|
||||
forceinline const T *operator->() const {
|
||||
auto non_const_this = const_cast<JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *>(this);
|
||||
non_const_this->flush();
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// Returns a pointer to the included object without flushing time.
|
||||
forceinline T *last_valid() {
|
||||
return &object_;
|
||||
@ -53,7 +60,8 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
if(!is_flushed_) {
|
||||
is_flushed_ = true;
|
||||
if constexpr (divider == 1) {
|
||||
object_.run_for(time_since_update_.template flush<TargetTimeScale>());
|
||||
const auto duration = time_since_update_.template flush<TargetTimeScale>();
|
||||
object_.run_for(duration);
|
||||
} else {
|
||||
const auto duration = time_since_update_.template divide<TargetTimeScale>(LocalTimeScale(divider));
|
||||
if(duration > TargetTimeScale(0))
|
||||
|
@ -83,8 +83,9 @@ template <class BusHandler> class MOS6560 {
|
||||
speaker_.set_input_rate(static_cast<float>(clock_rate / 4.0));
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); }
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); }
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); }
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const { return crt_.get_scaled_scan_status() / 4.0f; }
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); }
|
||||
Outputs::Speaker::Speaker *get_speaker() { return &speaker_; }
|
||||
|
||||
void set_high_frequency_cutoff(float cutoff) {
|
||||
|
@ -117,6 +117,14 @@ void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus TMS9918::get_scaled_scan_status() const {
|
||||
// The input was scaled by 3/4 to convert half cycles to internal ticks,
|
||||
// so undo that and also allow for: (i) the multiply by 4 that it takes
|
||||
// to reach the CRT; and (ii) the fact that the half-cycles value was scaled,
|
||||
// and this should really reply in whole cycles.
|
||||
return crt_.get_scaled_scan_status() * (4.0f / (3.0f * 8.0f));
|
||||
}
|
||||
|
||||
void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
@ -44,6 +44,9 @@ class TMS9918: public Base {
|
||||
/*! Sets the scan target this TMS will post content to. */
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/*! Sets the type of display the CRT will request. */
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
|
@ -335,6 +335,11 @@ class CRTCBusHandler {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// @returns The current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 64.0f;
|
||||
}
|
||||
|
||||
/// Sets the type of display.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
@ -1006,6 +1011,11 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
crtc_bus_handler_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
/// A CRTMachine function; returns the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return crtc_bus_handler_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
/// A CRTMachine function; sets the output display type.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override final {
|
||||
crtc_bus_handler_.set_display_type(display_type);
|
||||
|
@ -421,6 +421,10 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
video_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
/// Sets the type of display.
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
video_.set_display_type(display_type);
|
||||
|
@ -47,6 +47,10 @@ void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus VideoBase::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 14.0f;
|
||||
}
|
||||
|
||||
void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
@ -40,6 +40,9 @@ class VideoBase {
|
||||
/// Sets the scan target.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/// Sets the type of output.
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
|
@ -175,6 +175,10 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
video_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
return &audio_.speaker;
|
||||
}
|
||||
|
@ -37,6 +37,10 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 2.0f;
|
||||
}
|
||||
|
||||
void Video::run_for(HalfCycles duration) {
|
||||
// Determine the current video and audio bases. These values don't appear to be latched, they apply immediately.
|
||||
const size_t video_base = (use_alternate_screen_buffer_ ? (0xffff2700 >> 1) : (0xffffa700 >> 1)) & ram_mask_;
|
||||
|
@ -42,6 +42,9 @@ class Video {
|
||||
*/
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/*!
|
||||
Produces the next @c duration period of pixels.
|
||||
*/
|
||||
|
@ -77,12 +77,9 @@ using Target = Analyser::Static::Atari2600::Target;
|
||||
class ConcreteMachine:
|
||||
public Machine,
|
||||
public CRTMachine::Machine,
|
||||
public JoystickMachine::Machine,
|
||||
public Outputs::CRT::Delegate {
|
||||
public JoystickMachine::Machine {
|
||||
public:
|
||||
ConcreteMachine(const Target &target) {
|
||||
set_clock_rate(NTSC_clock_rate);
|
||||
|
||||
ConcreteMachine(const Target &target) : frequency_mismatch_warner_(*this) {
|
||||
const std::vector<uint8_t> &rom = target.media.cartridges.front()->get_segments().front().data;
|
||||
|
||||
using PagingModel = Target::PagingModel;
|
||||
@ -122,6 +119,8 @@ class ConcreteMachine:
|
||||
|
||||
joysticks_.emplace_back(new Joystick(bus_.get(), 0, 0));
|
||||
joysticks_.emplace_back(new Joystick(bus_.get(), 4, 1));
|
||||
|
||||
set_is_ntsc(is_ntsc_);
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
@ -157,10 +156,14 @@ class ConcreteMachine:
|
||||
// to satisfy CRTMachine::Machine
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
|
||||
bus_->speaker_.set_input_rate(static_cast<float>(get_clock_rate() / static_cast<double>(CPUTicksPerAudioTick)));
|
||||
bus_->tia_.set_crt_delegate(this);
|
||||
bus_->tia_.set_crt_delegate(&frequency_mismatch_warner_);
|
||||
bus_->tia_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return bus_->tia_.get_scaled_scan_status() / 3.0f;
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override {
|
||||
return &bus_->speaker_;
|
||||
}
|
||||
@ -170,42 +173,13 @@ class ConcreteMachine:
|
||||
bus_->apply_confidence(confidence_counter_);
|
||||
}
|
||||
|
||||
// to satisfy Outputs::CRT::Delegate
|
||||
void crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, int number_of_frames, int number_of_unexpected_vertical_syncs) override {
|
||||
const std::size_t number_of_frame_records = sizeof(frame_records_) / sizeof(frame_records_[0]);
|
||||
frame_records_[frame_record_pointer_ % number_of_frame_records].number_of_frames = number_of_frames;
|
||||
frame_records_[frame_record_pointer_ % number_of_frame_records].number_of_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs;
|
||||
frame_record_pointer_ ++;
|
||||
void flush() {
|
||||
bus_->flush();
|
||||
}
|
||||
|
||||
if(frame_record_pointer_ >= 6) {
|
||||
int total_number_of_frames = 0;
|
||||
int total_number_of_unexpected_vertical_syncs = 0;
|
||||
for(std::size_t c = 0; c < number_of_frame_records; c++) {
|
||||
total_number_of_frames += frame_records_[c].number_of_frames;
|
||||
total_number_of_unexpected_vertical_syncs += frame_records_[c].number_of_unexpected_vertical_syncs;
|
||||
}
|
||||
|
||||
if(total_number_of_unexpected_vertical_syncs >= total_number_of_frames >> 1) {
|
||||
for(std::size_t c = 0; c < number_of_frame_records; c++) {
|
||||
frame_records_[c].number_of_frames = 0;
|
||||
frame_records_[c].number_of_unexpected_vertical_syncs = 0;
|
||||
}
|
||||
is_ntsc_ ^= true;
|
||||
|
||||
double clock_rate;
|
||||
if(is_ntsc_) {
|
||||
clock_rate = NTSC_clock_rate;
|
||||
bus_->tia_.set_output_mode(TIA::OutputMode::NTSC);
|
||||
} else {
|
||||
clock_rate = PAL_clock_rate;
|
||||
bus_->tia_.set_output_mode(TIA::OutputMode::PAL);
|
||||
}
|
||||
|
||||
bus_->speaker_.set_input_rate(static_cast<float>(clock_rate / static_cast<double>(CPUTicksPerAudioTick)));
|
||||
bus_->speaker_.set_high_frequency_cutoff(static_cast<float>(clock_rate / (static_cast<double>(CPUTicksPerAudioTick) * 2.0)));
|
||||
set_clock_rate(clock_rate);
|
||||
}
|
||||
}
|
||||
void register_crt_frequency_mismatch() {
|
||||
is_ntsc_ ^= true;
|
||||
set_is_ntsc(is_ntsc_);
|
||||
}
|
||||
|
||||
float get_confidence() override {
|
||||
@ -213,20 +187,24 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
private:
|
||||
// the bus
|
||||
// The bus.
|
||||
std::unique_ptr<Bus> bus_;
|
||||
|
||||
// output frame rate tracker
|
||||
struct FrameRecord {
|
||||
int number_of_frames = 0;
|
||||
int number_of_unexpected_vertical_syncs = 0;
|
||||
} frame_records_[4];
|
||||
unsigned int frame_record_pointer_ = 0;
|
||||
// Output frame rate tracker.
|
||||
Outputs::CRT::CRTFrequencyMismatchWarner<ConcreteMachine> frequency_mismatch_warner_;
|
||||
bool is_ntsc_ = true;
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
|
||||
// a confidence counter
|
||||
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
|
||||
|
||||
void set_is_ntsc(bool is_ntsc) {
|
||||
bus_->tia_.set_output_mode(is_ntsc ? TIA::OutputMode::NTSC : TIA::OutputMode::PAL);
|
||||
const double clock_rate = is_ntsc ? NTSC_clock_rate : PAL_clock_rate;
|
||||
bus_->speaker_.set_input_rate(float(clock_rate) / float(CPUTicksPerAudioTick));
|
||||
bus_->speaker_.set_high_frequency_cutoff(float(clock_rate) / float(CPUTicksPerAudioTick * 2));
|
||||
set_clock_rate(clock_rate);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ class Bus {
|
||||
virtual void run_for(const Cycles cycles) = 0;
|
||||
virtual void apply_confidence(Analyser::Dynamic::ConfidenceCounter &confidence_counter) = 0;
|
||||
virtual void set_reset_line(bool state) = 0;
|
||||
virtual void flush() = 0;
|
||||
|
||||
// the RIOT, TIA and speaker
|
||||
PIA mos6532_;
|
||||
|
@ -39,7 +39,7 @@ template<class T> class Cartridge:
|
||||
// consider doing something less fragile.
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) {
|
||||
void run_for(const Cycles cycles) override {
|
||||
// Horizontal counter resets are used as a proxy for whether this really is an Atari 2600
|
||||
// title. Random memory accesses are likely to trigger random counter resets.
|
||||
horizontal_counter_resets_ = 0;
|
||||
@ -50,13 +50,13 @@ template<class T> class Cartridge:
|
||||
/*!
|
||||
Adjusts @c confidence_counter according to the results of the most recent run_for.
|
||||
*/
|
||||
void apply_confidence(Analyser::Dynamic::ConfidenceCounter &confidence_counter) {
|
||||
void apply_confidence(Analyser::Dynamic::ConfidenceCounter &confidence_counter) override {
|
||||
if(cycle_count_.as_integral() < 200) return;
|
||||
if(horizontal_counter_resets_ > 10)
|
||||
confidence_counter.add_miss();
|
||||
}
|
||||
|
||||
void set_reset_line(bool state) { m6502_.set_reset_line(state); }
|
||||
void set_reset_line(bool state) override { m6502_.set_reset_line(state); }
|
||||
|
||||
// to satisfy CPU::MOS6502::Processor
|
||||
Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
|
||||
@ -197,7 +197,7 @@ template<class T> class Cartridge:
|
||||
return Cycles(cycles_run_for / 3);
|
||||
}
|
||||
|
||||
void flush() {
|
||||
void flush() override {
|
||||
update_audio();
|
||||
update_video();
|
||||
audio_queue_.perform();
|
||||
|
@ -142,6 +142,10 @@ void TIA::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus TIA::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 2.0f;
|
||||
}
|
||||
|
||||
void TIA::run_for(const Cycles cycles) {
|
||||
int number_of_cycles = int(cycles.as_integral());
|
||||
|
||||
|
@ -75,6 +75,7 @@ class TIA {
|
||||
|
||||
void set_crt_delegate(Outputs::CRT::Delegate *);
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
private:
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
@ -139,6 +139,10 @@ class ConcreteMachine:
|
||||
video_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_->set_display_type(display_type);
|
||||
}
|
||||
|
@ -127,6 +127,10 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 2.0f;
|
||||
}
|
||||
|
||||
void Video::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
@ -40,6 +40,9 @@ class Video {
|
||||
*/
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/*!
|
||||
Sets the type of output.
|
||||
*/
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
#include "../Configurable/StandardOptions.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
// TODO: rename.
|
||||
@ -37,6 +38,13 @@ class Machine {
|
||||
*/
|
||||
virtual void set_scan_target(Outputs::Display::ScanTarget *scan_target) = 0;
|
||||
|
||||
/*!
|
||||
@returns The current scan status.
|
||||
*/
|
||||
virtual Outputs::Display::ScanStatus get_scan_status() const {
|
||||
return get_scaled_scan_status() / float(clock_rate_);
|
||||
}
|
||||
|
||||
/// @returns The speaker that receives this machine's output, or @c nullptr if this machine is mute.
|
||||
virtual Outputs::Speaker::Speaker *get_speaker() = 0;
|
||||
|
||||
@ -73,7 +81,10 @@ class Machine {
|
||||
|
||||
enum MachineEvent: int {
|
||||
/// At least one new packet of audio has been delivered to the spaker's delegate.
|
||||
NewSpeakerSamplesGenerated = 1 << 0
|
||||
NewSpeakerSamplesGenerated = 1 << 0,
|
||||
|
||||
/// The next vertical retrace has begun.
|
||||
VerticalSync = 1 << 1,
|
||||
};
|
||||
|
||||
/*!
|
||||
@ -93,10 +104,16 @@ class Machine {
|
||||
sample_sets = speaker->completed_sample_sets();
|
||||
}
|
||||
|
||||
int retraces = 0;
|
||||
if(events & MachineEvent::VerticalSync) {
|
||||
retraces = get_scan_status().hsync_count;
|
||||
}
|
||||
|
||||
// Run until all requested events are satisfied.
|
||||
return run_until(minimum_duration, [=]() {
|
||||
return
|
||||
(!(events & MachineEvent::NewSpeakerSamplesGenerated) || (sample_sets != speaker->completed_sample_sets()));
|
||||
(!(events & MachineEvent::NewSpeakerSamplesGenerated) || (sample_sets != speaker->completed_sample_sets())) &&
|
||||
(!(events & MachineEvent::VerticalSync) || (retraces != get_scan_status().hsync_count));
|
||||
});
|
||||
}
|
||||
|
||||
@ -110,6 +127,15 @@ class Machine {
|
||||
return clock_rate_;
|
||||
}
|
||||
|
||||
virtual Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
||||
// This deliberately sets up an infinite loop if the user hasn't
|
||||
// overridden at least one of this or get_scan_status.
|
||||
//
|
||||
// Most likely you want to override this, and let the base class
|
||||
// throw in a divide-by-clock-rate at the end for you.
|
||||
return get_scan_status();
|
||||
}
|
||||
|
||||
/*!
|
||||
Maps from Configurable::Display to Outputs::Display::VideoSignal and calls
|
||||
@c set_display_type with the result.
|
||||
@ -139,6 +165,7 @@ class Machine {
|
||||
*/
|
||||
virtual void set_display_type(Outputs::Display::DisplayType display_type) {}
|
||||
|
||||
|
||||
private:
|
||||
double clock_rate_ = 1.0;
|
||||
double clock_conversion_error_ = 0.0;
|
||||
|
@ -185,6 +185,10 @@ class ConcreteMachine:
|
||||
vdp_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return vdp_->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
vdp_->set_display_type(display_type);
|
||||
}
|
||||
|
@ -626,6 +626,10 @@ class ConcreteMachine:
|
||||
mos6560_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return mos6560_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override final {
|
||||
mos6560_.set_display_type(display_type);
|
||||
}
|
||||
|
@ -383,6 +383,10 @@ class ConcreteMachine:
|
||||
video_output_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_output_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
video_output_.set_display_type(display_type);
|
||||
}
|
||||
|
@ -56,6 +56,10 @@ void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus VideoOutput::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / float(crt_cycles_multiplier);
|
||||
}
|
||||
|
||||
void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
crt_.set_display_type(display_type);
|
||||
}
|
||||
|
@ -39,6 +39,9 @@ class VideoOutput {
|
||||
/// Sets the destination for output.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/// Sets the type of output.
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
|
@ -282,6 +282,10 @@ class ConcreteMachine:
|
||||
vdp_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return vdp_->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
vdp_->set_display_type(display_type);
|
||||
}
|
||||
|
@ -177,6 +177,10 @@ class ConcreteMachine:
|
||||
vdp_->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return vdp_->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) override {
|
||||
vdp_->set_display_type(display_type);
|
||||
}
|
||||
|
@ -556,6 +556,10 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
|
||||
video_output_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_output_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_output_.set_display_type(display_type);
|
||||
}
|
||||
|
@ -26,12 +26,27 @@ namespace {
|
||||
VideoOutput::VideoOutput(uint8_t *memory) :
|
||||
ram_(memory),
|
||||
crt_(64*6, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1),
|
||||
frequency_mismatch_warner_(*this),
|
||||
v_sync_start_position_(PAL50VSyncStartPosition), v_sync_end_position_(PAL50VSyncEndPosition),
|
||||
counter_period_(PAL50Period) {
|
||||
crt_.set_visible_area(crt_.get_rect_for_area(54, 224, 16 * 6, 40 * 6, 4.0f / 3.0f));
|
||||
crt_.set_phase_linked_luminance_offset(-1.0f / 8.0f);
|
||||
data_type_ = Outputs::Display::InputDataType::Red1Green1Blue1;
|
||||
crt_.set_input_data_type(data_type_);
|
||||
crt_.set_delegate(&frequency_mismatch_warner_);
|
||||
update_crt_frequency();
|
||||
}
|
||||
|
||||
void VideoOutput::register_crt_frequency_mismatch() {
|
||||
crt_is_60Hz_ ^= true;
|
||||
update_crt_frequency();
|
||||
}
|
||||
|
||||
void VideoOutput::update_crt_frequency() {
|
||||
// Set the proper frequency...
|
||||
crt_.set_new_display_type(64*6, crt_is_60Hz_ ? Outputs::Display::Type::PAL60 : Outputs::Display::Type::PAL50);
|
||||
|
||||
// ... but also pick an appropriate crop rectangle.
|
||||
crt_.set_visible_area(crt_.get_rect_for_area(crt_is_60Hz_ ? 26 : 54, 224, 16 * 6, 40 * 6, 4.0f / 3.0f));
|
||||
}
|
||||
|
||||
void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
@ -56,6 +71,10 @@ void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus VideoOutput::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 6.0f;
|
||||
}
|
||||
|
||||
void VideoOutput::set_colour_rom(const std::vector<uint8_t> &rom) {
|
||||
for(std::size_t c = 0; c < 8; c++) {
|
||||
colour_forms_[c] = 0;
|
||||
@ -88,8 +107,8 @@ void VideoOutput::set_colour_rom(const std::vector<uint8_t> &rom) {
|
||||
}
|
||||
|
||||
void VideoOutput::run_for(const Cycles cycles) {
|
||||
// Vertical: 0-39: pixels; otherwise blank; 48-53 sync, 54-56 colour burst
|
||||
// Horizontal: 0-223: pixels; otherwise blank; 256-259 sync
|
||||
// Horizontal: 0-39: pixels; otherwise blank; 48-53 sync, 54-56 colour burst.
|
||||
// Vertical: 0-223: pixels; otherwise blank; 256-259 (50Hz) or 234-238 (60Hz) sync.
|
||||
|
||||
#define clamp(action) \
|
||||
if(cycles_run_for <= number_of_cycles) { action; } else cycles_run_for = number_of_cycles;
|
||||
|
@ -27,22 +27,29 @@ class VideoOutput {
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
void set_display_type(Outputs::Display::DisplayType display_type);
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
void register_crt_frequency_mismatch();
|
||||
|
||||
private:
|
||||
uint8_t *ram_;
|
||||
Outputs::CRT::CRT crt_;
|
||||
Outputs::CRT::CRTFrequencyMismatchWarner<VideoOutput> frequency_mismatch_warner_;
|
||||
bool crt_is_60Hz_ = false;
|
||||
|
||||
// Counters and limits
|
||||
void update_crt_frequency();
|
||||
|
||||
// Counters and limits.
|
||||
int counter_ = 0, frame_counter_ = 0;
|
||||
int v_sync_start_position_, v_sync_end_position_, counter_period_;
|
||||
|
||||
// Output target and device
|
||||
// Output target and device.
|
||||
uint8_t *rgb_pixel_target_;
|
||||
uint32_t *composite_pixel_target_;
|
||||
uint32_t colour_forms_[8];
|
||||
Outputs::Display::InputDataType data_type_;
|
||||
|
||||
// Registers
|
||||
// Registers.
|
||||
uint8_t ink_, paper_;
|
||||
|
||||
int character_set_base_address_ = 0xb400;
|
||||
|
@ -109,3 +109,7 @@ void Video::output_byte(uint8_t byte) {
|
||||
void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
crt_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
|
||||
return crt_.get_scaled_scan_status() / 0.5f;
|
||||
}
|
||||
|
@ -36,12 +36,16 @@ class Video {
|
||||
|
||||
/// Sets the current sync output.
|
||||
void set_sync(bool sync);
|
||||
|
||||
/// Causes @c byte to be serialised into pixels and output over the next four cycles.
|
||||
void output_byte(uint8_t byte);
|
||||
|
||||
/// Sets the scan target.
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
|
||||
|
||||
/// Gets the current scan status.
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
private:
|
||||
bool sync_ = false;
|
||||
uint8_t *line_data_ = nullptr;
|
||||
|
@ -318,6 +318,10 @@ template<bool is_zx81> class ConcreteMachine:
|
||||
video_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() override final {
|
||||
return is_zx81 ? &speaker_ : nullptr;
|
||||
}
|
||||
|
@ -67,7 +67,7 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableASanStackUseAfterReturn = "YES"
|
||||
|
@ -225,16 +225,25 @@ class MachineDocument:
|
||||
}
|
||||
|
||||
func machineSpeakerDidChangeInputClock(_ machine: CSMachine) {
|
||||
setupAudioQueueClockRate()
|
||||
// setupAudioQueueClockRate not only needs blocking access to the machine,
|
||||
// but may be triggered on an arbitrary thread by a running machine, and that
|
||||
// running machine may not be able to stop running until it has been called
|
||||
// (e.g. if it is currently trying to run_until an audio event). Break the
|
||||
// deadlock with an async dispatch.
|
||||
DispatchQueue.main.async {
|
||||
self.setupAudioQueueClockRate()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupAudioQueueClockRate() {
|
||||
// establish and provide the audio queue, taking advice as to an appropriate sampling rate
|
||||
// Establish and provide the audio queue, taking advice as to an appropriate sampling rate.
|
||||
//
|
||||
// TODO: this needs to be threadsafe. FIX!
|
||||
let maximumSamplingRate = CSAudioQueue.preferredSamplingRate()
|
||||
let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate)))
|
||||
if selectedSamplingRate > 0 {
|
||||
audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate))
|
||||
audioQueue.delegate = self
|
||||
self.audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate))
|
||||
self.audioQueue.delegate = self
|
||||
self.machine.audioQueue = self.audioQueue
|
||||
self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize)
|
||||
}
|
||||
|
@ -327,6 +327,11 @@ struct ActivityObserver: public Activity::Observer {
|
||||
|
||||
- (void)updateViewForPixelSize:(CGSize)pixelSize {
|
||||
_scanTarget->update((int)pixelSize.width, (int)pixelSize.height);
|
||||
|
||||
// @synchronized(self) {
|
||||
// const auto scan_status = _machine->crt_machine()->get_scan_status();
|
||||
// NSLog(@"FPS (hopefully): %0.2f [retrace: %0.4f]", 1.0f / scan_status.field_duration, scan_status.retrace_duration);
|
||||
// }
|
||||
}
|
||||
|
||||
- (void)drawViewForPixelSize:(CGSize)pixelSize {
|
||||
|
@ -111,8 +111,10 @@ void CRT::set_brightness(float brightness) {
|
||||
void CRT::set_new_display_type(int cycles_per_line, Outputs::Display::Type displayType) {
|
||||
switch(displayType) {
|
||||
case Outputs::Display::Type::PAL50:
|
||||
case Outputs::Display::Type::PAL60:
|
||||
scan_target_modals_.intended_gamma = 2.8f;
|
||||
set_new_timing(cycles_per_line, 312, Outputs::Display::ColourSpace::YUV, 709379, 2500, 5, true); // i.e. 283.7516 colour cycles per line; 2.5 lines = vertical sync.
|
||||
set_new_timing(cycles_per_line, (displayType == Outputs::Display::Type::PAL50) ? 312 : 262, Outputs::Display::ColourSpace::YUV, 709379, 2500, 5, true);
|
||||
// i.e. 283.7516 colour cycles per line; 2.5 lines = vertical sync.
|
||||
break;
|
||||
|
||||
case Outputs::Display::Type::NTSC60:
|
||||
@ -418,7 +420,9 @@ void CRT::output_data(int number_of_cycles, size_t number_of_samples) {
|
||||
output_scan(&scan);
|
||||
}
|
||||
|
||||
Outputs::Display::Rect CRT::get_rect_for_area(int first_line_after_sync, int number_of_lines, int first_cycle_after_sync, int number_of_cycles, float aspect_ratio) {
|
||||
// MARK: - Getters.
|
||||
|
||||
Outputs::Display::Rect CRT::get_rect_for_area(int first_line_after_sync, int number_of_lines, int first_cycle_after_sync, int number_of_cycles, float aspect_ratio) const {
|
||||
first_cycle_after_sync *= time_multiplier_;
|
||||
number_of_cycles *= time_multiplier_;
|
||||
|
||||
@ -465,3 +469,16 @@ Outputs::Display::Rect CRT::get_rect_for_area(int first_line_after_sync, int num
|
||||
|
||||
return Outputs::Display::Rect(start_x, start_y, width, height);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus CRT::get_scaled_scan_status() const {
|
||||
Outputs::Display::ScanStatus status;
|
||||
status.field_duration = float(vertical_flywheel_->get_locked_period()) / float(time_multiplier_);
|
||||
status.field_duration_gradient = float(vertical_flywheel_->get_last_period_adjustment()) / float(time_multiplier_);
|
||||
status.retrace_duration = float(vertical_flywheel_->get_retrace_period()) / float(time_multiplier_);
|
||||
status.current_position =
|
||||
std::max(0.0f,
|
||||
float(vertical_flywheel_->get_current_output_position()) / (float(vertical_flywheel_->get_locked_period()) * float(time_multiplier_))
|
||||
);
|
||||
status.hsync_count = vertical_flywheel_->get_number_of_retraces();
|
||||
return status;
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
#ifndef CRT_hpp
|
||||
#define CRT_hpp
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
@ -264,7 +265,7 @@ class CRT {
|
||||
int number_of_lines,
|
||||
int first_cycle_after_sync,
|
||||
int number_of_cycles,
|
||||
float aspect_ratio);
|
||||
float aspect_ratio) const;
|
||||
|
||||
/*! Sets the CRT delegate; set to @c nullptr if no delegate is desired. */
|
||||
inline void set_delegate(Delegate *delegate) {
|
||||
@ -274,6 +275,12 @@ class CRT {
|
||||
/*! Sets the scan target for CRT output. */
|
||||
void set_scan_target(Outputs::Display::ScanTarget *);
|
||||
|
||||
/*!
|
||||
Gets current scan status, with time based fields being in the input scale — e.g. if you're supplying
|
||||
86 cycles/line and 98 lines/field then it'll return a field duration of 86*98.
|
||||
*/
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const;
|
||||
|
||||
/*! Sets the display type that will be nominated to the scan target. */
|
||||
void set_display_type(Outputs::Display::DisplayType);
|
||||
|
||||
@ -287,6 +294,51 @@ class CRT {
|
||||
void set_brightness(float);
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides a CRT delegate that will will observe sync mismatches and, when an appropriate threshold is crossed,
|
||||
ask its receiver to try a different display frequency.
|
||||
*/
|
||||
template <typename Receiver> class CRTFrequencyMismatchWarner: public Outputs::CRT::Delegate {
|
||||
public:
|
||||
CRTFrequencyMismatchWarner(Receiver &receiver) : receiver_(receiver) {}
|
||||
|
||||
void crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, int number_of_frames, int number_of_unexpected_vertical_syncs) final {
|
||||
frame_records_[frame_record_pointer_ % frame_records_.size()].number_of_frames = number_of_frames;
|
||||
frame_records_[frame_record_pointer_ % frame_records_.size()].number_of_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs;
|
||||
++frame_record_pointer_;
|
||||
|
||||
if(frame_record_pointer_*2 >= frame_records_.size()*3) {
|
||||
int total_number_of_frames = 0;
|
||||
int total_number_of_unexpected_vertical_syncs = 0;
|
||||
for(const auto &record: frame_records_) {
|
||||
total_number_of_frames += record.number_of_frames;
|
||||
total_number_of_unexpected_vertical_syncs += record.number_of_unexpected_vertical_syncs;
|
||||
}
|
||||
|
||||
if(total_number_of_unexpected_vertical_syncs >= total_number_of_frames >> 1) {
|
||||
reset();
|
||||
receiver_.register_crt_frequency_mismatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
for(auto &record: frame_records_) {
|
||||
record.number_of_frames = 0;
|
||||
record.number_of_unexpected_vertical_syncs = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Receiver &receiver_;
|
||||
struct FrameRecord {
|
||||
int number_of_frames = 0;
|
||||
int number_of_unexpected_vertical_syncs = 0;
|
||||
};
|
||||
std::array<FrameRecord, 4> frame_records_;
|
||||
size_t frame_record_pointer_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,6 +63,7 @@ struct Flywheel {
|
||||
inline SyncEvent get_next_event_in_period(bool sync_is_requested, int cycles_to_run_for, int *cycles_advanced) {
|
||||
// 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_ = (3*expected_next_sync_ + time_now) >> 2;
|
||||
@ -75,6 +76,7 @@ struct Flywheel {
|
||||
expected_next_sync_ = (3*expected_next_sync_ + standard_period_ - sync_error_window_) >> 2;
|
||||
}
|
||||
}
|
||||
last_adjustment_ = expected_next_sync_ - last_sync;
|
||||
}
|
||||
|
||||
SyncEvent proposed_event = SyncEvent::None;
|
||||
@ -165,6 +167,20 @@ struct Flywheel {
|
||||
return standard_period_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns the actual current period for a complete scan (including retrace).
|
||||
*/
|
||||
inline int get_locked_period() {
|
||||
return expected_next_sync_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns the amount by which the @c locked_period was adjusted, the last time that an adjustment was applied.
|
||||
*/
|
||||
inline int get_last_period_adjustment() {
|
||||
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.
|
||||
@ -175,6 +191,20 @@ struct Flywheel {
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns A count of the number of retraces so far performed.
|
||||
*/
|
||||
inline int get_number_of_retraces() {
|
||||
return number_of_retraces_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The amount of time this flywheel spends in retrace, as supplied at construction.
|
||||
*/
|
||||
inline int get_retrace_period() {
|
||||
return retrace_time_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns `true` if a sync is expected soon or if the time at which it was expected (or received) was recent.
|
||||
*/
|
||||
@ -185,15 +215,18 @@ struct Flywheel {
|
||||
}
|
||||
|
||||
private:
|
||||
const int standard_period_; // the normal length of time between syncs
|
||||
const int retrace_time_; // a constant indicating the amount of time it takes to perform a retrace
|
||||
const int sync_error_window_; // a constant indicating the window either side of the next expected sync in which we'll accept other syncs
|
||||
const int standard_period_; // The idealised length of time between syncs.
|
||||
const int retrace_time_; // A constant indicating the amount of time it takes to perform a retrace.
|
||||
const int sync_error_window_; // A constant indicating the window either side of the next expected sync in which we'll accept other syncs.
|
||||
|
||||
int counter_ = 0; // time since the _start_ of the last sync
|
||||
int counter_before_retrace_; // the value of _counter immediately before retrace began
|
||||
int expected_next_sync_; // our current expection of when the next sync will be encountered (which implies velocity)
|
||||
int counter_ = 0; // Time since the _start_ of the last sync.
|
||||
int counter_before_retrace_; // The value of _counter immediately before retrace began.
|
||||
int expected_next_sync_; // Our current expection of when the next sync will be encountered (which implies velocity).
|
||||
|
||||
int number_of_surprises_ = 0; // a count of the surprising syncs
|
||||
int number_of_surprises_ = 0; // A count of the surprising syncs.
|
||||
int number_of_retraces_ = 0; // A count of the number of retraces to date.
|
||||
|
||||
int last_adjustment_ = 0; // The amount by which expected_next_sync_ was adjusted at the last sync.
|
||||
|
||||
/*
|
||||
Implementation notes:
|
||||
|
@ -11,12 +11,14 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include "../ClockReceiver/TimeTypes.hpp"
|
||||
|
||||
namespace Outputs {
|
||||
namespace Display {
|
||||
|
||||
enum class Type {
|
||||
PAL50,
|
||||
PAL60,
|
||||
NTSC60
|
||||
};
|
||||
|
||||
@ -309,6 +311,54 @@ struct ScanTarget {
|
||||
virtual void announce(Event event, bool is_visible, const Scan::EndPoint &location, uint8_t composite_amplitude) {}
|
||||
};
|
||||
|
||||
struct ScanStatus {
|
||||
/// The current (prediced) length of a field (including retrace).
|
||||
Time::Seconds field_duration;
|
||||
/// The difference applied to the field_duration estimate during the last field.
|
||||
Time::Seconds field_duration_gradient;
|
||||
/// The amount of time this device spends in retrace.
|
||||
Time::Seconds retrace_duration;
|
||||
/// The distance into the current field, from a small negative amount (in retrace) through
|
||||
/// 0 (start of visible area field) to 1 (end of field).
|
||||
///
|
||||
/// This will increase monotonically, being a measure
|
||||
/// of the current vertical position — i.e. if current_position = 0.8 then a caller can
|
||||
/// conclude that the top 80% of the visible part of the display has been painted.
|
||||
float current_position;
|
||||
/// The total number of hsyncs so far encountered;
|
||||
int hsync_count;
|
||||
/// @c true if retrace is currently going on; @c false otherwise.
|
||||
bool is_in_retrace;
|
||||
|
||||
/*!
|
||||
@returns this ScanStatus, with time-relative fields scaled by dividing them by @c dividend.
|
||||
*/
|
||||
ScanStatus operator / (float dividend) {
|
||||
const ScanStatus result = {
|
||||
.field_duration = field_duration / dividend,
|
||||
.field_duration_gradient = field_duration_gradient / dividend,
|
||||
.retrace_duration = retrace_duration / dividend,
|
||||
.current_position = current_position,
|
||||
.hsync_count = hsync_count,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns this ScanStatus, with time-relative fields scaled by multiplying them by @c multiplier.
|
||||
*/
|
||||
ScanStatus operator * (float multiplier) {
|
||||
const ScanStatus result = {
|
||||
.field_duration = field_duration * multiplier,
|
||||
.field_duration_gradient = field_duration_gradient * multiplier,
|
||||
.retrace_duration = retrace_duration * multiplier,
|
||||
.current_position = current_position,
|
||||
.hsync_count = hsync_count,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides a null target for scans.
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user