1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-10-03 10:54:46 +00:00

Merge pull request #737 from TomHarte/Multisync

Adds multisync monitor support to the Oric.
This commit is contained in:
Thomas Harte 2020-01-23 22:20:34 -05:00 committed by GitHub
commit 2103e1b470
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 395 additions and 78 deletions

View File

@ -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_;
}

View File

@ -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;

View File

@ -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))

View File

@ -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) {

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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_;

View File

@ -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.
*/

View File

@ -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);
}
};
}

View File

@ -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_;

View File

@ -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();

View File

@ -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());

View File

@ -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_;

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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.
*/

View File

@ -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;

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -67,7 +67,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableASanStackUseAfterReturn = "YES"

View File

@ -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)
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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;
};
}
}

View File

@ -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:

View File

@ -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.
*/