1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-23 03:32:32 +00:00
CLK/Machines/CRTMachine.hpp
Thomas Harte 394ee61c78 Starts a switch to reflectable-style runtime options.
The Amstrad CPC and ZX80/81 have made the jump so far, subject to caveats. The macOS build is unlikely currently to work properly.
2020-03-16 23:25:05 -04:00

216 lines
6.8 KiB
C++

//
// CRTMachine.hpp
// Clock Signal
//
// Created by Thomas Harte on 31/05/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#ifndef CRTMachine_hpp
#define CRTMachine_hpp
#include "../Outputs/ScanTarget.hpp"
#include "../Outputs/Speaker/Speaker.hpp"
#include "../ClockReceiver/ClockReceiver.hpp"
#include "../ClockReceiver/TimeTypes.hpp"
#include "ROMMachine.hpp"
#include "../Configurable/StandardOptions.hpp"
#include <array>
#include <cmath>
// TODO: rename.
namespace CRTMachine {
/*!
A CRTMachine::Machine is a mostly-abstract base class for machines that connect to a CRT,
that optionally provide a speaker, and that nominate a clock rate and can announce to a delegate
should that clock rate change.
*/
class Machine {
public:
/*!
Causes the machine to set up its display and, if it has one, speaker.
The @c scan_target will receive all video output; the caller guarantees
that it is non-null.
*/
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;
/// @returns The confidence that this machine is running content it understands.
virtual float get_confidence() { return 0.5f; }
virtual std::string debug_type() { return ""; }
/// Runs the machine for @c duration seconds.
virtual void run_for(Time::Seconds duration) {
const double cycles = (duration * clock_rate_ * speed_multiplier_) + clock_conversion_error_;
clock_conversion_error_ = std::fmod(cycles, 1.0);
run_for(Cycles(static_cast<int>(cycles)));
}
/*!
Sets a speed multiplier to apply to this machine; e.g. a multiplier of 1.5 will cause the
emulated machine to run 50% faster than a real machine. This speed-up is an emulation
fiction: it will apply across the system, including to the CRT.
*/
virtual void set_speed_multiplier(double multiplier) {
speed_multiplier_ = multiplier;
auto speaker = get_speaker();
if(speaker) {
speaker->set_input_rate_multiplier(float(multiplier));
}
}
/*!
@returns The current speed multiplier.
*/
virtual double get_speed_multiplier() {
return speed_multiplier_;
}
/*!
Runs for the machine for at least @c duration seconds, and then until @c condition is true.
@returns The amount of time run for.
*/
Time::Seconds run_until(Time::Seconds minimum_duration, std::function<bool()> condition) {
Time::Seconds total_runtime = minimum_duration;
run_for(minimum_duration);
while(!condition()) {
// Advance in increments of one 500th of a second until the condition
// is true; that's 1/10th of a 50Hz frame, but more like 1/8.33 of a
// 60Hz frame. Though most machines aren't exactly 50Hz or 60Hz, and some
// are arbitrary other refresh rates. So those observations are merely
// for scale.
run_for(0.002);
total_runtime += 0.002;
}
return total_runtime;
}
enum MachineEvent: int {
/// At least one new packet of audio has been delivered to the spaker's delegate.
NewSpeakerSamplesGenerated = 1 << 0,
/// The next vertical retrace has begun.
VerticalSync = 1 << 1,
};
/*!
Runs for at least @c duration seconds, and then every one of the @c events has occurred at least once since this
call to @c run_until_event.
@param events A bitmask comprised of @c MachineEvent flags.
@returns The amount of time run for.
*/
Time::Seconds run_until(Time::Seconds minimum_duration, int events) {
// Tie up a wait-for-samples, if requested.
const Outputs::Speaker::Speaker *speaker = nullptr;
int sample_sets = 0;
if(events & MachineEvent::NewSpeakerSamplesGenerated) {
speaker = get_speaker();
if(!speaker) events &= ~MachineEvent::NewSpeakerSamplesGenerated;
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::VerticalSync) || (retraces != get_scan_status().hsync_count));
});
}
protected:
/// Runs the machine for @c cycles.
virtual void run_for(const Cycles cycles) = 0;
void set_clock_rate(double clock_rate) {
clock_rate_ = clock_rate;
}
double get_clock_rate() {
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.
*/
void set_video_signal_configurable(Configurable::Display type) {
Outputs::Display::DisplayType display_type;
switch(type) {
default:
case Configurable::Display::RGB:
display_type = Outputs::Display::DisplayType::RGB;
break;
case Configurable::Display::SVideo:
display_type = Outputs::Display::DisplayType::SVideo;
break;
case Configurable::Display::CompositeColour:
display_type = Outputs::Display::DisplayType::CompositeColour;
break;
case Configurable::Display::CompositeMonochrome:
display_type = Outputs::Display::DisplayType::CompositeMonochrome;
break;
}
set_display_type(display_type);
}
/*!
Maps back from Outputs::Display::VideoSignal to Configurable::Display,
calling @c get_display_type for the input.
*/
Configurable::Display get_video_signal_configurable() {
switch(get_display_type()) {
default:
case Outputs::Display::DisplayType::RGB: return Configurable::Display::RGB;
case Outputs::Display::DisplayType::SVideo: return Configurable::Display::SVideo;
case Outputs::Display::DisplayType::CompositeColour: return Configurable::Display::CompositeColour;
case Outputs::Display::DisplayType::CompositeMonochrome: return Configurable::Display::CompositeMonochrome;
}
}
/*!
Sets the display type.
*/
virtual void set_display_type(Outputs::Display::DisplayType display_type) {}
/*!
Gets the display type.
*/
virtual Outputs::Display::DisplayType get_display_type() { return Outputs::Display::DisplayType::RGB; }
private:
double clock_rate_ = 1.0;
double clock_conversion_error_ = 0.0;
double speed_multiplier_ = 1.0;
};
}
#endif /* CRTMachine_hpp */