mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-08 03:17:17 +00:00
Compare commits
17 Commits
2025-10-03
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
60b3c51085 | ||
|
d7b5a45417 | ||
|
e11060bde8 | ||
|
4653de9161 | ||
|
1926ad9215 | ||
|
33d047c703 | ||
|
fadda00246 | ||
|
a3fed788d8 | ||
|
dde31e8687 | ||
|
190fb009bc | ||
|
62574d04c6 | ||
|
2496257bcf | ||
|
ab73b4de6b | ||
|
6c1c32baca | ||
|
239cc15c8f | ||
|
6b437c3907 | ||
|
4756f63169 |
@@ -13,6 +13,8 @@
|
||||
|
||||
#include "Machines/MachineTypes.hpp"
|
||||
|
||||
#include "Outputs/CRT/MismatchWarner.hpp"
|
||||
|
||||
#include "Analyser/Static/Atari2600/Target.hpp"
|
||||
|
||||
#include "Cartridges/Atari8k.hpp"
|
||||
|
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Outputs/CRT/CRT.hpp"
|
||||
#include "Outputs/CRT/MismatchWarner.hpp"
|
||||
#include "ClockReceiver/ClockReceiver.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
@@ -186,7 +186,9 @@ public:
|
||||
|
||||
InstructionSet::x86::SegmentRegisterSet<Descriptor> descriptors;
|
||||
|
||||
auto operator <=>(const Segments &rhs) const = default;
|
||||
auto operator ==(const Segments &rhs) const {
|
||||
return descriptors == rhs.descriptors;
|
||||
}
|
||||
|
||||
private:
|
||||
void load_real(const Source segment) {
|
||||
|
69
Numeric/CubicCurve.hpp
Normal file
69
Numeric/CubicCurve.hpp
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// CubicCurve.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 04/10/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
|
||||
/*!
|
||||
Provides a cubic Bezier-based timing function.
|
||||
*/
|
||||
struct CubicCurve {
|
||||
CubicCurve(const float c1x, const float c1y, const float c2x, const float c2y) :
|
||||
c1(c1x, c1y), c2(c2x, c2y)
|
||||
{
|
||||
assert(0.0f <= c1x); assert(c1x <= 1.0f);
|
||||
assert(0.0f <= c1y); assert(c1y <= 1.0f);
|
||||
assert(0.0f <= c2x); assert(c2x <= 1.0f);
|
||||
assert(0.0f <= c2y); assert(c2y <= 1.0f);
|
||||
}
|
||||
|
||||
/// @returns A standard ease-in-out animation curve.
|
||||
static CubicCurve easeInOut() {
|
||||
return CubicCurve(0.42f, 0.0f, 0.58f, 1.0f);
|
||||
}
|
||||
|
||||
/// @returns The value for y given x, in range [0.0, 1.0].
|
||||
float value(const float x) const {
|
||||
return axis(t(x), 1);
|
||||
}
|
||||
|
||||
private:
|
||||
/// @returns The value for @c t that generates the value @c x.
|
||||
float t(const float x) const {
|
||||
static constexpr float Precision = 0.01f;
|
||||
float bounds[2] = {0.0f, 1.0f};
|
||||
const auto midpoint = [&] { return (bounds[0] + bounds[1]) * 0.5f; };
|
||||
|
||||
while(bounds[1] > bounds[0] + Precision) {
|
||||
const float mid = midpoint();
|
||||
const float value = axis(mid, 0);
|
||||
if(value > x) {
|
||||
bounds[1] = mid;
|
||||
} else {
|
||||
bounds[0] = mid;
|
||||
}
|
||||
}
|
||||
return midpoint();
|
||||
}
|
||||
|
||||
/// @returns The value for axis @c index at time @c t.
|
||||
float axis(const float t, int index) const {
|
||||
const float f1 = t * c1[index];
|
||||
const float f2 = t * c2[index] + (1.0f - t) * c1[index];
|
||||
const float f3 = t + (1.0f - t) * c2[index];
|
||||
|
||||
const float c1 = t * f2 + (1.0f - t) * f1;
|
||||
const float c2 = t * f3 + (1.0f - t) * f2;
|
||||
|
||||
return t * c2 + (1.0f - t) * c1;
|
||||
}
|
||||
|
||||
float c1[2];
|
||||
float c2[2];
|
||||
};
|
@@ -26,6 +26,9 @@ template <typename Full, typename Half> union alignas(Full) alignas(Half) Regist
|
||||
auto operator <=>(const RegisterPair &rhs) const {
|
||||
return full <=> rhs.full;
|
||||
}
|
||||
auto operator ==(const RegisterPair &rhs) const {
|
||||
return full == rhs.full;
|
||||
}
|
||||
#if TARGET_RT_BIG_ENDIAN
|
||||
struct {
|
||||
Half high, low;
|
||||
|
@@ -1780,6 +1780,8 @@
|
||||
4B83348E1F5DBA6E0097E338 /* 6522Storage.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = 6522Storage.hpp; path = Implementation/6522Storage.hpp; sourceTree = "<group>"; };
|
||||
4B8334911F5E24FF0097E338 /* C1540Base.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = C1540Base.hpp; path = Implementation/C1540Base.hpp; sourceTree = "<group>"; };
|
||||
4B8334941F5E25B60097E338 /* C1540.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = C1540.cpp; path = Implementation/C1540.cpp; sourceTree = "<group>"; };
|
||||
4B847B9F2E920C7500774B9B /* CubicCurve.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = CubicCurve.hpp; path = /Users/thomasharte/Projects/CLK/Numeric/CubicCurve.hpp; sourceTree = "<absolute>"; };
|
||||
4B847BA42E94320B00774B9B /* MismatchWarner.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MismatchWarner.hpp; sourceTree = "<group>"; };
|
||||
4B85322922778E4200F26553 /* Comparative68000.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Comparative68000.hpp; sourceTree = "<group>"; };
|
||||
4B85322E2277ABDD00F26553 /* tos100.trace.txt.gz */ = {isa = PBXFileReference; lastKnownFileType = archive.gzip; path = tos100.trace.txt.gz; sourceTree = "<group>"; };
|
||||
4B8671EA2D8B40D8009E1610 /* Descriptors.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Descriptors.hpp; sourceTree = "<group>"; };
|
||||
@@ -2797,6 +2799,7 @@
|
||||
children = (
|
||||
4B0CCC421C62D0B3001CAC5F /* CRT.cpp */,
|
||||
4B0CCC431C62D0B3001CAC5F /* CRT.hpp */,
|
||||
4B847BA42E94320B00774B9B /* MismatchWarner.hpp */,
|
||||
4BBF99071C8FBA6F0075DAFB /* Internals */,
|
||||
);
|
||||
path = CRT;
|
||||
@@ -3147,6 +3150,7 @@
|
||||
4BD191D5219113B80042E144 /* OpenGL */,
|
||||
4BB8616B24E22DC500A00E03 /* ScanTargets */,
|
||||
4BD060A41FE49D3C006E14BE /* Speaker */,
|
||||
4B847B9F2E920C7500774B9B /* CubicCurve.hpp */,
|
||||
);
|
||||
name = Outputs;
|
||||
path = ../../Outputs;
|
||||
|
@@ -160,7 +160,7 @@ private:
|
||||
@interface CPCShakerTests : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation CPCShakerTests {}
|
||||
@implementation CPCShakerTests
|
||||
|
||||
- (void)testCSLPath:(NSString *)path name:(NSString *)name {
|
||||
using namespace Storage::Automation;
|
||||
|
@@ -15,25 +15,38 @@
|
||||
|
||||
using namespace Outputs::CRT;
|
||||
|
||||
void CRT::set_new_timing(int cycles_per_line, int height_of_display, Outputs::Display::ColourSpace colour_space, int colour_cycle_numerator, int colour_cycle_denominator, int vertical_sync_half_lines, bool should_alternate) {
|
||||
// MARK: - Input timing setup.
|
||||
|
||||
static constexpr int millisecondsHorizontalRetraceTime = 7; // Source: Dictionary of Video and Television Technology, p. 234.
|
||||
static constexpr int scanlinesVerticalRetraceTime = 8; // Source: ibid.
|
||||
void CRT::set_new_timing(
|
||||
const int cycles_per_line,
|
||||
const int height_of_display,
|
||||
const Outputs::Display::ColourSpace colour_space,
|
||||
const int colour_cycle_numerator,
|
||||
const int colour_cycle_denominator,
|
||||
const int vertical_sync_half_lines,
|
||||
const bool should_alternate
|
||||
) {
|
||||
static constexpr int HorizontalRetraceMs = 7; // Source: Dictionary of Video and Television Technology, p. 234.
|
||||
static constexpr int VerticalRetraceLines = 8; // Source: ibid.
|
||||
|
||||
// To quote:
|
||||
//
|
||||
// "retrace interval; The interval of time for the return of the blanked scanning beam of
|
||||
// a TV picture tube or camera tube to the starting point of a line or field. It is about
|
||||
// 7 microseconds for horizontal retrace and 500 to 750 microseconds for vertical retrace
|
||||
// in NTSC and PAL TV."
|
||||
// To quote:
|
||||
//
|
||||
// "retrace interval; The interval of time for the return of the blanked scanning beam of
|
||||
// a TV picture tube or camera tube to the starting point of a line or field. It is about
|
||||
// 7 microseconds for horizontal retrace and 500 to 750 microseconds for vertical retrace
|
||||
// in NTSC and PAL TV."
|
||||
|
||||
|
||||
// 63475 = 65535 * 31/32, i.e. the same 1/32 error as below is permitted.
|
||||
time_multiplier_ = 63487 / cycles_per_line;
|
||||
|
||||
time_multiplier_ = 63487 / cycles_per_line; // 63475 = 65535 * 31/32, i.e. the same 1/32 error as below is permitted.
|
||||
phase_denominator_ = int64_t(cycles_per_line) * int64_t(colour_cycle_denominator) * int64_t(time_multiplier_);
|
||||
phase_numerator_ = 0;
|
||||
colour_cycle_numerator_ = int64_t(colour_cycle_numerator);
|
||||
phase_alternates_ = should_alternate;
|
||||
should_be_alternate_line_ &= phase_alternates_;
|
||||
cycles_per_line_ = cycles_per_line;
|
||||
|
||||
const int multiplied_cycles_per_line = cycles_per_line * time_multiplier_;
|
||||
|
||||
// Allow sync to be detected (and acted upon) a line earlier than the specified requirement,
|
||||
@@ -41,25 +54,33 @@ void CRT::set_new_timing(int cycles_per_line, int height_of_display, Outputs::Di
|
||||
// the gist for simple debugging.
|
||||
sync_capacitor_charge_threshold_ = ((vertical_sync_half_lines - 2) * cycles_per_line) >> 1;
|
||||
|
||||
// Create the two flywheels:
|
||||
//
|
||||
// The horizontal flywheel has an ideal period of `multiplied_cycles_per_line`, will accept syncs
|
||||
// within 1/32nd of that (i.e. tolerates 3.125% error) and takes millisecondsHorizontalRetraceTime
|
||||
// Horizontal flywheel: has an ideal period of `multiplied_cycles_per_line`, will accept syncs
|
||||
// within 1/32nd of that (i.e. tolerates 3.125% error) and takes HorizontalRetraceMs
|
||||
// to retrace.
|
||||
//
|
||||
// The vertical slywheel has an ideal period of `multiplied_cycles_per_line * height_of_display`,
|
||||
// will accept syncs within 1/8th of that (i.e. tolerates 12.5% error) and takes scanlinesVerticalRetraceTime
|
||||
horizontal_flywheel_ =
|
||||
Flywheel(
|
||||
multiplied_cycles_per_line,
|
||||
(HorizontalRetraceMs * multiplied_cycles_per_line) >> 6,
|
||||
multiplied_cycles_per_line >> 5
|
||||
);
|
||||
|
||||
// Vertical flywheel: has an ideal period of `multiplied_cycles_per_line * height_of_display`,
|
||||
// will accept syncs within 1/8th of that (i.e. tolerates 12.5% error) and takes VerticalRetraceLines
|
||||
// to retrace.
|
||||
horizontal_flywheel_ = std::make_unique<Flywheel>(multiplied_cycles_per_line, (millisecondsHorizontalRetraceTime * multiplied_cycles_per_line) >> 6, multiplied_cycles_per_line >> 5);
|
||||
vertical_flywheel_ = std::make_unique<Flywheel>(multiplied_cycles_per_line * height_of_display, scanlinesVerticalRetraceTime * multiplied_cycles_per_line, (multiplied_cycles_per_line * height_of_display) >> 3);
|
||||
vertical_flywheel_ =
|
||||
Flywheel(
|
||||
multiplied_cycles_per_line * height_of_display,
|
||||
VerticalRetraceLines * multiplied_cycles_per_line,
|
||||
(multiplied_cycles_per_line * height_of_display) >> 3
|
||||
);
|
||||
|
||||
// Figure out the divisor necessary to get the horizontal flywheel into a 16-bit range.
|
||||
const int real_clock_scan_period = vertical_flywheel_->get_scan_period();
|
||||
const int real_clock_scan_period = vertical_flywheel_.scan_period();
|
||||
vertical_flywheel_output_divider_ = (real_clock_scan_period + 65534) / 65535;
|
||||
|
||||
// Communicate relevant fields to the scan target.
|
||||
scan_target_modals_.cycles_per_line = cycles_per_line;
|
||||
scan_target_modals_.output_scale.x = uint16_t(horizontal_flywheel_->get_scan_period());
|
||||
scan_target_modals_.output_scale.x = uint16_t(horizontal_flywheel_.scan_period());
|
||||
scan_target_modals_.output_scale.y = uint16_t(real_clock_scan_period / vertical_flywheel_output_divider_);
|
||||
scan_target_modals_.expected_vertical_lines = height_of_display;
|
||||
scan_target_modals_.composite_colour_space = colour_space;
|
||||
@@ -68,52 +89,7 @@ void CRT::set_new_timing(int cycles_per_line, int height_of_display, Outputs::Di
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||||
scan_target_ = scan_target;
|
||||
if(!scan_target_) scan_target_ = &Outputs::Display::NullScanTarget::singleton;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_new_data_type(Outputs::Display::InputDataType data_type) {
|
||||
scan_target_modals_.input_data_type = data_type;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_aspect_ratio(float aspect_ratio) {
|
||||
scan_target_modals_.aspect_ratio = aspect_ratio;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_visible_area(Outputs::Display::Rect visible_area) {
|
||||
scan_target_modals_.visible_area = visible_area;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_display_type(Outputs::Display::DisplayType display_type) {
|
||||
scan_target_modals_.display_type = display_type;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType CRT::get_display_type() const {
|
||||
return scan_target_modals_.display_type;
|
||||
}
|
||||
|
||||
void CRT::set_phase_linked_luminance_offset(float offset) {
|
||||
scan_target_modals_.input_data_tweaks.phase_linked_luminance_offset = offset;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_input_data_type(Outputs::Display::InputDataType input_data_type) {
|
||||
scan_target_modals_.input_data_type = input_data_type;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_brightness(float brightness) {
|
||||
scan_target_modals_.brightness = brightness;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_new_display_type(int cycles_per_line, Outputs::Display::Type displayType) {
|
||||
void CRT::set_new_display_type(const int cycles_per_line, const Outputs::Display::Type displayType) {
|
||||
switch(displayType) {
|
||||
case Outputs::Display::Type::PAL50:
|
||||
case Outputs::Display::Type::PAL60:
|
||||
@@ -142,7 +118,7 @@ void CRT::set_new_display_type(int cycles_per_line, Outputs::Display::Type displ
|
||||
}
|
||||
}
|
||||
|
||||
void CRT::set_composite_function_type(CompositeSourceType type, float offset_of_first_sample) {
|
||||
void CRT::set_composite_function_type(const CompositeSourceType type, const float offset_of_first_sample) {
|
||||
if(type == DiscreteFourSamplesPerCycle) {
|
||||
colour_burst_phase_adjustment_ = uint8_t(offset_of_first_sample * 256.0f) & 63;
|
||||
} else {
|
||||
@@ -150,125 +126,141 @@ void CRT::set_composite_function_type(CompositeSourceType type, float offset_of_
|
||||
}
|
||||
}
|
||||
|
||||
void CRT::set_input_gamma(float gamma) {
|
||||
scan_target_modals_.intended_gamma = gamma;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
// MARK: - Constructors.
|
||||
|
||||
CRT::CRT( int cycles_per_line,
|
||||
int clocks_per_pixel_greatest_common_divisor,
|
||||
int height_of_display,
|
||||
Outputs::Display::ColourSpace colour_space,
|
||||
int colour_cycle_numerator, int colour_cycle_denominator,
|
||||
int vertical_sync_half_lines,
|
||||
bool should_alternate,
|
||||
Outputs::Display::InputDataType data_type) {
|
||||
CRT::CRT(
|
||||
const int cycles_per_line,
|
||||
const int clocks_per_pixel_greatest_common_divisor,
|
||||
const int height_of_display,
|
||||
const Outputs::Display::ColourSpace colour_space,
|
||||
const int colour_cycle_numerator, int colour_cycle_denominator,
|
||||
const int vertical_sync_half_lines,
|
||||
const bool should_alternate,
|
||||
const Outputs::Display::InputDataType data_type
|
||||
) {
|
||||
scan_target_modals_.input_data_type = data_type;
|
||||
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
|
||||
set_new_timing(cycles_per_line, height_of_display, colour_space, colour_cycle_numerator, colour_cycle_denominator, vertical_sync_half_lines, should_alternate);
|
||||
set_new_timing(
|
||||
cycles_per_line,
|
||||
height_of_display,
|
||||
colour_space,
|
||||
colour_cycle_numerator,
|
||||
colour_cycle_denominator,
|
||||
vertical_sync_half_lines,
|
||||
should_alternate
|
||||
);
|
||||
}
|
||||
|
||||
CRT::CRT( int cycles_per_line,
|
||||
int clocks_per_pixel_greatest_common_divisor,
|
||||
Outputs::Display::Type display_type,
|
||||
Outputs::Display::InputDataType data_type) {
|
||||
CRT::CRT(
|
||||
const int cycles_per_line,
|
||||
const int clocks_per_pixel_greatest_common_divisor,
|
||||
const Outputs::Display::Type display_type,
|
||||
const Outputs::Display::InputDataType data_type
|
||||
) {
|
||||
scan_target_modals_.input_data_type = data_type;
|
||||
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
|
||||
set_new_display_type(cycles_per_line, display_type);
|
||||
}
|
||||
|
||||
CRT::CRT(int cycles_per_line,
|
||||
int clocks_per_pixel_greatest_common_divisor,
|
||||
int height_of_display,
|
||||
int vertical_sync_half_lines,
|
||||
Outputs::Display::InputDataType data_type) {
|
||||
CRT::CRT(
|
||||
const int cycles_per_line,
|
||||
const int clocks_per_pixel_greatest_common_divisor,
|
||||
const int height_of_display,
|
||||
const int vertical_sync_half_lines,
|
||||
const Outputs::Display::InputDataType data_type
|
||||
) {
|
||||
scan_target_modals_.input_data_type = data_type;
|
||||
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
|
||||
set_new_timing(cycles_per_line, height_of_display, Outputs::Display::ColourSpace::YIQ, 1, 1, vertical_sync_half_lines, false);
|
||||
set_new_timing(
|
||||
cycles_per_line,
|
||||
height_of_display,
|
||||
Outputs::Display::ColourSpace::YIQ,
|
||||
1,
|
||||
1,
|
||||
vertical_sync_half_lines,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Use some from-thin-air arbitrary constants for default timing, otherwise passing
|
||||
// construction off to one of the other constructors.
|
||||
CRT::CRT(Outputs::Display::InputDataType data_type) : CRT(100, 1, 100, 1, data_type) {}
|
||||
CRT::CRT(const Outputs::Display::InputDataType data_type) : CRT(100, 1, 100, 1, data_type) {}
|
||||
|
||||
// MARK: - Sync loop
|
||||
|
||||
Flywheel::SyncEvent CRT::get_next_vertical_sync_event(bool vsync_is_requested, int cycles_to_run_for, int *cycles_advanced) {
|
||||
return vertical_flywheel_->get_next_event_in_period(vsync_is_requested, cycles_to_run_for, cycles_advanced);
|
||||
}
|
||||
|
||||
Flywheel::SyncEvent CRT::get_next_horizontal_sync_event(bool hsync_is_requested, int cycles_to_run_for, int *cycles_advanced) {
|
||||
return horizontal_flywheel_->get_next_event_in_period(hsync_is_requested, cycles_to_run_for, cycles_advanced);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanTarget::Scan::EndPoint CRT::end_point(uint16_t data_offset) {
|
||||
Display::ScanTarget::Scan::EndPoint end_point;
|
||||
|
||||
// Clamp the available range on endpoints. These will almost always be within range, but may go
|
||||
// out during times of resync.
|
||||
end_point.x = uint16_t(std::min(horizontal_flywheel_->get_current_output_position(), 65535));
|
||||
end_point.y = uint16_t(std::min(vertical_flywheel_->get_current_output_position() / vertical_flywheel_output_divider_, 65535));
|
||||
end_point.data_offset = data_offset;
|
||||
|
||||
// Ensure .composite_angle is sampled at the location indicated by .cycles_since_end_of_horizontal_retrace.
|
||||
// TODO: I could supply time_multiplier_ as a modal and just not round .cycles_since_end_of_horizontal_retrace. Would that be better?
|
||||
const auto lost_precision = cycles_since_horizontal_sync_ % time_multiplier_;
|
||||
end_point.composite_angle = int16_t(((phase_numerator_ - lost_precision * colour_cycle_numerator_) << 6) / phase_denominator_) * (is_alternate_line_ ? -1 : 1);
|
||||
end_point.cycles_since_end_of_horizontal_retrace = uint16_t(cycles_since_horizontal_sync_ / time_multiplier_);
|
||||
|
||||
return end_point;
|
||||
}
|
||||
|
||||
void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type, int number_of_samples) {
|
||||
void CRT::advance_cycles(
|
||||
int number_of_cycles,
|
||||
bool hsync_requested,
|
||||
bool vsync_requested,
|
||||
const Scan::Type type,
|
||||
const int number_of_samples
|
||||
) {
|
||||
number_of_cycles *= time_multiplier_;
|
||||
|
||||
const bool is_output_run = ((type == Scan::Type::Level) || (type == Scan::Type::Data));
|
||||
const bool is_output_run = (type == Scan::Type::Level) || (type == Scan::Type::Data);
|
||||
const auto total_cycles = number_of_cycles;
|
||||
bool did_output = false;
|
||||
const auto end_point = [&] {
|
||||
return this->end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles));
|
||||
};
|
||||
|
||||
while(number_of_cycles) {
|
||||
|
||||
// Get time until next horizontal and vertical sync generator events.
|
||||
int time_until_vertical_sync_event, time_until_horizontal_sync_event;
|
||||
const Flywheel::SyncEvent next_vertical_sync_event = get_next_vertical_sync_event(vsync_requested, number_of_cycles, &time_until_vertical_sync_event);
|
||||
const Flywheel::SyncEvent next_horizontal_sync_event = get_next_horizontal_sync_event(hsync_requested, time_until_vertical_sync_event, &time_until_horizontal_sync_event);
|
||||
const auto vertical_event = vertical_flywheel_.next_event_in_period(vsync_requested, number_of_cycles);
|
||||
assert(vertical_event.second >= 0 && vertical_event.second <= number_of_cycles);
|
||||
|
||||
const auto horizontal_event = horizontal_flywheel_.next_event_in_period(hsync_requested, vertical_event.second);
|
||||
assert(horizontal_event.second >= 0 && horizontal_event.second <= vertical_event.second);
|
||||
|
||||
// Whichever event is scheduled to happen first is the one to advance to.
|
||||
const int next_run_length = std::min(time_until_vertical_sync_event, time_until_horizontal_sync_event);
|
||||
const int next_run_length = horizontal_event.second;
|
||||
|
||||
// Request each sync at most once.
|
||||
hsync_requested = false;
|
||||
vsync_requested = false;
|
||||
|
||||
// Determine whether to output any data for this portion of the output; if so then grab somewhere to put it.
|
||||
const bool is_output_segment = ((is_output_run && next_run_length) && !horizontal_flywheel_->is_in_retrace() && !vertical_flywheel_->is_in_retrace());
|
||||
const bool is_output_segment =
|
||||
is_output_run &&
|
||||
next_run_length &&
|
||||
!horizontal_flywheel_.is_in_retrace() &&
|
||||
!vertical_flywheel_.is_in_retrace();
|
||||
Outputs::Display::ScanTarget::Scan *const next_scan = is_output_segment ? scan_target_->begin_scan() : nullptr;
|
||||
did_output |= is_output_segment;
|
||||
|
||||
// If outputting, store the start location and scan constants.
|
||||
if(next_scan) {
|
||||
next_scan->end_points[0] = end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles));
|
||||
next_scan->end_points[0] = end_point();
|
||||
next_scan->composite_amplitude = colour_burst_amplitude_;
|
||||
}
|
||||
|
||||
// Advance time: that'll affect both the colour subcarrier position and the number of cycles left to run.
|
||||
// Advance time: that'll affect both the colour subcarrier and the number of cycles left to run.
|
||||
phase_numerator_ += next_run_length * colour_cycle_numerator_;
|
||||
number_of_cycles -= next_run_length;
|
||||
cycles_since_horizontal_sync_ += next_run_length;
|
||||
|
||||
// React to the incoming event.
|
||||
horizontal_flywheel_->apply_event(next_run_length, (next_run_length == time_until_horizontal_sync_event) ? next_horizontal_sync_event : Flywheel::SyncEvent::None);
|
||||
vertical_flywheel_->apply_event(next_run_length, (next_run_length == time_until_vertical_sync_event) ? next_vertical_sync_event : Flywheel::SyncEvent::None);
|
||||
horizontal_flywheel_.apply_event(
|
||||
next_run_length,
|
||||
next_run_length == horizontal_event.second ? horizontal_event.first : Flywheel::SyncEvent::None
|
||||
);
|
||||
vertical_flywheel_.apply_event(
|
||||
next_run_length,
|
||||
next_run_length == vertical_event.second ? vertical_event.first : Flywheel::SyncEvent::None
|
||||
);
|
||||
|
||||
// End the scan if necessary.
|
||||
if(next_scan) {
|
||||
next_scan->end_points[1] = end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles));
|
||||
next_scan->end_points[1] = end_point();
|
||||
scan_target_->end_scan();
|
||||
}
|
||||
|
||||
// Announce horizontal retrace events.
|
||||
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event != Flywheel::SyncEvent::None) {
|
||||
using Event = Outputs::Display::ScanTarget::Event;
|
||||
|
||||
// Announce horizontal sync events.
|
||||
if(next_run_length == horizontal_event.second && horizontal_event.first != Flywheel::SyncEvent::None) {
|
||||
// Reset the cycles-since-sync counter if this is the end of retrace.
|
||||
if(next_horizontal_sync_event == Flywheel::SyncEvent::EndRetrace) {
|
||||
if(horizontal_event.first == Flywheel::SyncEvent::EndRetrace) {
|
||||
cycles_since_horizontal_sync_ = 0;
|
||||
|
||||
// This is unnecessary, strictly speaking, but seeks to help ScanTargets fit as
|
||||
@@ -279,39 +271,43 @@ void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_
|
||||
|
||||
// Announce event.
|
||||
const auto event =
|
||||
(next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace)
|
||||
? Outputs::Display::ScanTarget::Event::BeginHorizontalRetrace : Outputs::Display::ScanTarget::Event::EndHorizontalRetrace;
|
||||
horizontal_event.first == Flywheel::SyncEvent::StartRetrace
|
||||
? Event::BeginHorizontalRetrace : Event::EndHorizontalRetrace;
|
||||
scan_target_->announce(
|
||||
event,
|
||||
!(horizontal_flywheel_->is_in_retrace() || vertical_flywheel_->is_in_retrace()),
|
||||
end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles)),
|
||||
!(horizontal_flywheel_.is_in_retrace() || vertical_flywheel_.is_in_retrace()),
|
||||
end_point(),
|
||||
colour_burst_amplitude_);
|
||||
|
||||
// If retrace is starting, update phase if required and mark no colour burst spotted yet.
|
||||
if(next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace) {
|
||||
if(horizontal_event.first == Flywheel::SyncEvent::StartRetrace) {
|
||||
should_be_alternate_line_ ^= phase_alternates_;
|
||||
colour_burst_amplitude_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Also announce vertical retrace events.
|
||||
if(next_run_length == time_until_vertical_sync_event && next_vertical_sync_event != Flywheel::SyncEvent::None) {
|
||||
// Announce vertical sync events.
|
||||
if(next_run_length == vertical_event.second && vertical_event.first != Flywheel::SyncEvent::None) {
|
||||
const auto event =
|
||||
(next_vertical_sync_event == Flywheel::SyncEvent::StartRetrace)
|
||||
? Outputs::Display::ScanTarget::Event::BeginVerticalRetrace : Outputs::Display::ScanTarget::Event::EndVerticalRetrace;
|
||||
vertical_event.first == Flywheel::SyncEvent::StartRetrace
|
||||
? Event::BeginVerticalRetrace : Event::EndVerticalRetrace;
|
||||
scan_target_->announce(
|
||||
event,
|
||||
!(horizontal_flywheel_->is_in_retrace() || vertical_flywheel_->is_in_retrace()),
|
||||
end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles)),
|
||||
!(horizontal_flywheel_.is_in_retrace() || vertical_flywheel_.is_in_retrace()),
|
||||
end_point(),
|
||||
colour_burst_amplitude_);
|
||||
}
|
||||
|
||||
// if this is vertical retrace then advance a field
|
||||
if(next_run_length == time_until_vertical_sync_event && next_vertical_sync_event == Flywheel::SyncEvent::EndRetrace) {
|
||||
// At vertical retrace advance a field.
|
||||
if(next_run_length == vertical_event.second && vertical_event.first == Flywheel::SyncEvent::EndRetrace) {
|
||||
if(delegate_) {
|
||||
frames_since_last_delegate_call_++;
|
||||
++frames_since_last_delegate_call_;
|
||||
if(frames_since_last_delegate_call_ == 20) {
|
||||
delegate_->crt_did_end_batch_of_frames(*this, frames_since_last_delegate_call_, vertical_flywheel_->get_and_reset_number_of_surprises());
|
||||
delegate_->crt_did_end_batch_of_frames(
|
||||
*this,
|
||||
frames_since_last_delegate_call_,
|
||||
vertical_flywheel_.get_and_reset_number_of_surprises()
|
||||
);
|
||||
frames_since_last_delegate_call_ = 0;
|
||||
}
|
||||
}
|
||||
@@ -323,16 +319,42 @@ void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - stream feeding methods
|
||||
Outputs::Display::ScanTarget::Scan::EndPoint CRT::end_point(const uint16_t data_offset) {
|
||||
// Ensure .composite_angle is sampled at the location indicated by .cycles_since_end_of_horizontal_retrace.
|
||||
// TODO: I could supply time_multiplier_ as a modal and just not round .cycles_since_end_of_horizontal_retrace.
|
||||
// Would that be better?
|
||||
const auto lost_precision = cycles_since_horizontal_sync_ % time_multiplier_;
|
||||
const auto composite_angle =
|
||||
(((phase_numerator_ - lost_precision * colour_cycle_numerator_) << 6) / phase_denominator_)
|
||||
* (is_alternate_line_ ? -1 : 1);
|
||||
|
||||
void CRT::output_scan(const Scan *const scan) {
|
||||
assert(scan->number_of_cycles >= 0);
|
||||
return Display::ScanTarget::Scan::EndPoint{
|
||||
// Clamp the available range on endpoints. These will almost always be within range, but may go
|
||||
// out during times of resync.
|
||||
.x = uint16_t(std::min(horizontal_flywheel_.current_output_position(), 65535)),
|
||||
.y = uint16_t(
|
||||
std::min(vertical_flywheel_.current_output_position() / vertical_flywheel_output_divider_, 65535)
|
||||
),
|
||||
.data_offset = data_offset,
|
||||
|
||||
.composite_angle = int16_t(composite_angle),
|
||||
.cycles_since_end_of_horizontal_retrace = uint16_t(cycles_since_horizontal_sync_ / time_multiplier_),
|
||||
};
|
||||
}
|
||||
|
||||
// MARK: - Stream feeding.
|
||||
|
||||
void CRT::output_scan(const Scan &scan) {
|
||||
assert(scan.number_of_cycles >= 0);
|
||||
|
||||
// Simplified colour burst logic: if it's within the back porch we'll take it.
|
||||
if(scan->type == Scan::Type::ColourBurst) {
|
||||
if(!colour_burst_amplitude_ && horizontal_flywheel_->get_current_time() < (horizontal_flywheel_->get_standard_period() * 12) >> 6) {
|
||||
if(scan.type == Scan::Type::ColourBurst) {
|
||||
if(
|
||||
!colour_burst_amplitude_ &&
|
||||
horizontal_flywheel_.current_time() < (horizontal_flywheel_.standard_period() * 12) >> 6
|
||||
) {
|
||||
// Load phase_numerator_ as a fixed-point quantity in the range [0, 255].
|
||||
phase_numerator_ = scan->phase;
|
||||
phase_numerator_ = scan.phase;
|
||||
if(colour_burst_phase_adjustment_ != 0xff)
|
||||
phase_numerator_ = (phase_numerator_ & ~63) + colour_burst_phase_adjustment_;
|
||||
|
||||
@@ -340,21 +362,21 @@ void CRT::output_scan(const Scan *const scan) {
|
||||
phase_numerator_ = (phase_numerator_ * phase_denominator_) >> 8;
|
||||
|
||||
// Crib the colour burst amplitude.
|
||||
colour_burst_amplitude_ = scan->amplitude;
|
||||
colour_burst_amplitude_ = scan.amplitude;
|
||||
}
|
||||
}
|
||||
// TODO: inspect raw data for potential colour burst if required; the DPLL and some zero crossing logic
|
||||
// will probably be sufficient but some test data would be helpful
|
||||
|
||||
// sync logic: mark whether this is currently sync and check for a leading edge
|
||||
const bool this_is_sync = (scan->type == Scan::Type::Sync);
|
||||
const bool is_leading_edge = (!is_receiving_sync_ && this_is_sync);
|
||||
const bool this_is_sync = scan.type == Scan::Type::Sync;
|
||||
const bool is_leading_edge = !is_receiving_sync_ && this_is_sync;
|
||||
is_receiving_sync_ = this_is_sync;
|
||||
|
||||
// Horizontal sync is recognised on any leading edge that is not 'near' the expected vertical sync;
|
||||
// the second limb is to avoid slightly horizontal sync shifting from the common pattern of
|
||||
// equalisation pulses as the inverse of ordinary horizontal sync.
|
||||
bool hsync_requested = is_leading_edge && !vertical_flywheel_->is_near_expected_sync();
|
||||
bool hsync_requested = is_leading_edge && !vertical_flywheel_.is_near_expected_sync();
|
||||
|
||||
if(this_is_sync) {
|
||||
// If this is sync then either begin or continue a sync accumulation phase.
|
||||
@@ -363,7 +385,7 @@ void CRT::output_scan(const Scan *const scan) {
|
||||
} else {
|
||||
// If this is not sync then check how long it has been since sync. If it's more than
|
||||
// half a line then end sync accumulation and zero out the accumulating count.
|
||||
cycles_since_sync_ += scan->number_of_cycles;
|
||||
cycles_since_sync_ += scan.number_of_cycles;
|
||||
if(cycles_since_sync_ > (cycles_per_line_ >> 2)) {
|
||||
cycles_of_sync_ = 0;
|
||||
is_accumulating_sync_ = false;
|
||||
@@ -371,19 +393,19 @@ void CRT::output_scan(const Scan *const scan) {
|
||||
}
|
||||
}
|
||||
|
||||
int number_of_cycles = scan->number_of_cycles;
|
||||
int number_of_cycles = scan.number_of_cycles;
|
||||
bool vsync_requested = false;
|
||||
|
||||
// If sync is being accumulated then accumulate it; if it crosses the vertical sync threshold then
|
||||
// divide this line at the crossing point and indicate vertical sync there.
|
||||
if(is_accumulating_sync_ && !is_refusing_sync_) {
|
||||
cycles_of_sync_ += scan->number_of_cycles;
|
||||
cycles_of_sync_ += scan.number_of_cycles;
|
||||
|
||||
if(this_is_sync && cycles_of_sync_ >= sync_capacitor_charge_threshold_) {
|
||||
const int overshoot = std::min(cycles_of_sync_ - sync_capacitor_charge_threshold_, number_of_cycles);
|
||||
if(overshoot) {
|
||||
number_of_cycles -= overshoot;
|
||||
advance_cycles(number_of_cycles, hsync_requested, false, scan->type, 0);
|
||||
advance_cycles(number_of_cycles, hsync_requested, false, scan.type, 0);
|
||||
hsync_requested = false;
|
||||
number_of_cycles = overshoot;
|
||||
}
|
||||
@@ -393,7 +415,7 @@ void CRT::output_scan(const Scan *const scan) {
|
||||
}
|
||||
}
|
||||
|
||||
advance_cycles(number_of_cycles, hsync_requested, vsync_requested, scan->type, scan->number_of_samples);
|
||||
advance_cycles(number_of_cycles, hsync_requested, vsync_requested, scan.type, scan.number_of_samples);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -403,14 +425,14 @@ void CRT::output_sync(int number_of_cycles) {
|
||||
Scan scan;
|
||||
scan.type = Scan::Type::Sync;
|
||||
scan.number_of_cycles = number_of_cycles;
|
||||
output_scan(&scan);
|
||||
output_scan(scan);
|
||||
}
|
||||
|
||||
void CRT::output_blank(int number_of_cycles) {
|
||||
Scan scan;
|
||||
scan.type = Scan::Type::Blank;
|
||||
scan.number_of_cycles = number_of_cycles;
|
||||
output_scan(&scan);
|
||||
output_scan(scan);
|
||||
}
|
||||
|
||||
void CRT::output_level(int number_of_cycles) {
|
||||
@@ -419,7 +441,7 @@ void CRT::output_level(int number_of_cycles) {
|
||||
scan.type = Scan::Type::Level;
|
||||
scan.number_of_cycles = number_of_cycles;
|
||||
scan.number_of_samples = 1;
|
||||
output_scan(&scan);
|
||||
output_scan(scan);
|
||||
}
|
||||
|
||||
void CRT::output_colour_burst(int number_of_cycles, uint8_t phase, bool is_alternate_line, uint8_t amplitude) {
|
||||
@@ -429,12 +451,17 @@ void CRT::output_colour_burst(int number_of_cycles, uint8_t phase, bool is_alter
|
||||
scan.phase = phase;
|
||||
scan.amplitude = amplitude >> 1;
|
||||
is_alternate_line_ = is_alternate_line;
|
||||
output_scan(&scan);
|
||||
output_scan(scan);
|
||||
}
|
||||
|
||||
void CRT::output_default_colour_burst(int number_of_cycles, uint8_t amplitude) {
|
||||
// TODO: avoid applying a rounding error here?
|
||||
output_colour_burst(number_of_cycles, uint8_t((phase_numerator_ * 256) / phase_denominator_), should_be_alternate_line_, amplitude);
|
||||
output_colour_burst(
|
||||
number_of_cycles,
|
||||
uint8_t((phase_numerator_ * 256) / phase_denominator_),
|
||||
should_be_alternate_line_,
|
||||
amplitude
|
||||
);
|
||||
}
|
||||
|
||||
void CRT::set_immediate_default_phase(float phase) {
|
||||
@@ -453,9 +480,10 @@ void CRT::output_data(int number_of_cycles, size_t number_of_samples) {
|
||||
scan.type = Scan::Type::Data;
|
||||
scan.number_of_cycles = number_of_cycles;
|
||||
scan.number_of_samples = int(number_of_samples);
|
||||
output_scan(&scan);
|
||||
output_scan(scan);
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Getters.
|
||||
|
||||
Outputs::Display::Rect CRT::get_rect_for_area(
|
||||
@@ -463,7 +491,7 @@ Outputs::Display::Rect CRT::get_rect_for_area(
|
||||
int number_of_lines,
|
||||
int first_cycle_after_sync,
|
||||
int number_of_cycles,
|
||||
float aspect_ratio
|
||||
const float aspect_ratio
|
||||
) const {
|
||||
assert(number_of_cycles > 0);
|
||||
assert(number_of_lines > 0);
|
||||
@@ -478,8 +506,8 @@ Outputs::Display::Rect CRT::get_rect_for_area(
|
||||
number_of_lines += 4;
|
||||
|
||||
// Determine prima facie x extent.
|
||||
const int horizontal_period = horizontal_flywheel_->get_standard_period();
|
||||
const int horizontal_scan_period = horizontal_flywheel_->get_scan_period();
|
||||
const int horizontal_period = horizontal_flywheel_.standard_period();
|
||||
const int horizontal_scan_period = horizontal_flywheel_.scan_period();
|
||||
const int horizontal_retrace_period = horizontal_period - horizontal_scan_period;
|
||||
|
||||
// Ensure requested range is within visible region.
|
||||
@@ -490,8 +518,8 @@ Outputs::Display::Rect CRT::get_rect_for_area(
|
||||
float width = float(number_of_cycles) / float(horizontal_scan_period);
|
||||
|
||||
// Determine prima facie y extent.
|
||||
const int vertical_period = vertical_flywheel_->get_standard_period();
|
||||
const int vertical_scan_period = vertical_flywheel_->get_scan_period();
|
||||
const int vertical_period = vertical_flywheel_.standard_period();
|
||||
const int vertical_scan_period = vertical_flywheel_.scan_period();
|
||||
const int vertical_retrace_period = vertical_period - vertical_scan_period;
|
||||
|
||||
// Ensure range is visible.
|
||||
@@ -527,11 +555,63 @@ Outputs::Display::Rect CRT::get_rect_for_area(
|
||||
}
|
||||
|
||||
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 = float(vertical_flywheel_->get_current_phase()) / float(vertical_flywheel_->get_locked_scan_period());
|
||||
status.hsync_count = vertical_flywheel_->get_number_of_retraces();
|
||||
return status;
|
||||
return Outputs::Display::ScanStatus{
|
||||
.field_duration = float(vertical_flywheel_.locked_period()) / float(time_multiplier_),
|
||||
.field_duration_gradient = float(vertical_flywheel_.last_period_adjustment()) / float(time_multiplier_),
|
||||
.retrace_duration = float(vertical_flywheel_.retrace_period()) / float(time_multiplier_),
|
||||
.current_position = float(vertical_flywheel_.current_phase()) / float(vertical_flywheel_.locked_scan_period()),
|
||||
.hsync_count = vertical_flywheel_.number_of_retraces(),
|
||||
};
|
||||
}
|
||||
|
||||
// MARK: - ScanTarget passthroughs.
|
||||
|
||||
void CRT::set_scan_target(Outputs::Display::ScanTarget *const scan_target) {
|
||||
scan_target_ = scan_target;
|
||||
if(!scan_target_) scan_target_ = &Outputs::Display::NullScanTarget::singleton;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_new_data_type(const Outputs::Display::InputDataType data_type) {
|
||||
scan_target_modals_.input_data_type = data_type;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_aspect_ratio(const float aspect_ratio) {
|
||||
scan_target_modals_.aspect_ratio = aspect_ratio;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_visible_area(const Outputs::Display::Rect visible_area) {
|
||||
scan_target_modals_.visible_area = visible_area;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_display_type(const Outputs::Display::DisplayType display_type) {
|
||||
scan_target_modals_.display_type = display_type;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType CRT::get_display_type() const {
|
||||
return scan_target_modals_.display_type;
|
||||
}
|
||||
|
||||
void CRT::set_phase_linked_luminance_offset(const float offset) {
|
||||
scan_target_modals_.input_data_tweaks.phase_linked_luminance_offset = offset;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_input_data_type(const Outputs::Display::InputDataType input_data_type) {
|
||||
scan_target_modals_.input_data_type = input_data_type;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_brightness(const float brightness) {
|
||||
scan_target_modals_.brightness = brightness;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
||||
void CRT::set_input_gamma(const float gamma) {
|
||||
scan_target_modals_.intended_gamma = gamma;
|
||||
scan_target_->set_modals(scan_target_modals_);
|
||||
}
|
||||
|
@@ -45,7 +45,7 @@ static constexpr bool AlternatesPhase = false;
|
||||
class CRT;
|
||||
|
||||
struct Delegate {
|
||||
virtual void crt_did_end_batch_of_frames(CRT &, int number_of_frames, int number_of_unexpected_vertical_syncs) = 0;
|
||||
virtual void crt_did_end_batch_of_frames(CRT &, int frames, int unexpected_vertical_syncs) = 0;
|
||||
};
|
||||
|
||||
/*! Models a class 2d analogue output device, accepting a serial stream of data including syncs
|
||||
@@ -54,60 +54,6 @@ struct Delegate {
|
||||
colour phase for colour composite video.
|
||||
*/
|
||||
class CRT {
|
||||
private:
|
||||
// The incoming clock lengths will be multiplied by @c time_multiplier_; this increases
|
||||
// precision across the line.
|
||||
int time_multiplier_ = 1;
|
||||
|
||||
// Two flywheels regulate scanning; the vertical will have a range much greater than the horizontal;
|
||||
// the output divider is what that'll need to be divided by to reduce it into a 16-bit range as
|
||||
// posted on to the scan target.
|
||||
std::unique_ptr<Flywheel> horizontal_flywheel_, vertical_flywheel_;
|
||||
int vertical_flywheel_output_divider_ = 1;
|
||||
int cycles_since_horizontal_sync_ = 0;
|
||||
Display::ScanTarget::Scan::EndPoint end_point(uint16_t data_offset);
|
||||
|
||||
struct Scan {
|
||||
enum Type {
|
||||
Sync, Level, Data, Blank, ColourBurst
|
||||
} type = Scan::Blank;
|
||||
int number_of_cycles = 0, number_of_samples = 0;
|
||||
uint8_t phase = 0, amplitude = 0;
|
||||
};
|
||||
void output_scan(const Scan *scan);
|
||||
|
||||
uint8_t colour_burst_amplitude_ = 30;
|
||||
int colour_burst_phase_adjustment_ = 0xff;
|
||||
|
||||
int64_t phase_denominator_ = 1;
|
||||
int64_t phase_numerator_ = 0;
|
||||
int64_t colour_cycle_numerator_ = 1;
|
||||
bool is_alternate_line_ = false, phase_alternates_ = false, should_be_alternate_line_ = false;
|
||||
|
||||
void advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type, int number_of_samples);
|
||||
Flywheel::SyncEvent get_next_vertical_sync_event(bool vsync_is_requested, int cycles_to_run_for, int *cycles_advanced);
|
||||
Flywheel::SyncEvent get_next_horizontal_sync_event(bool hsync_is_requested, int cycles_to_run_for, int *cycles_advanced);
|
||||
|
||||
Delegate *delegate_ = nullptr;
|
||||
int frames_since_last_delegate_call_ = 0;
|
||||
|
||||
bool is_receiving_sync_ = false; // @c true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync); @c false otherwise.
|
||||
bool is_accumulating_sync_ = false; // @c true if a sync level has triggered the suspicion that a vertical sync might be in progress; @c false otherwise.
|
||||
bool is_refusing_sync_ = false; // @c true once a vertical sync has been detected, until a prolonged period of non-sync has ended suspicion of an ongoing vertical sync.
|
||||
int sync_capacitor_charge_threshold_ = 0; // Charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync.
|
||||
int cycles_of_sync_ = 0; // The number of cycles since the potential vertical sync began.
|
||||
int cycles_since_sync_ = 0; // The number of cycles since last in sync, for defeating the possibility of this being a vertical sync.
|
||||
|
||||
int cycles_per_line_ = 1;
|
||||
|
||||
Outputs::Display::ScanTarget *scan_target_ = &Outputs::Display::NullScanTarget::singleton;
|
||||
Outputs::Display::ScanTarget::Modals scan_target_modals_;
|
||||
static constexpr uint8_t DefaultAmplitude = 41; // Based upon a black level to maximum excursion and positive burst peak of: NTSC: 882 & 143; PAL: 933 & 150.
|
||||
|
||||
#ifndef NDEBUG
|
||||
size_t allocated_data_length_ = std::numeric_limits<size_t>::min();
|
||||
#endif
|
||||
|
||||
public:
|
||||
/*! Constructs the CRT with a specified clock rate, height and colour subcarrier frequency.
|
||||
The requested number of buffers, each with the requested number of bytes per pixel,
|
||||
@@ -129,18 +75,16 @@ public:
|
||||
|
||||
@param vertical_sync_half_lines The expected length of vertical synchronisation (equalisation pulses aside),
|
||||
in multiples of half a line.
|
||||
|
||||
@param data_type The format that the caller will use for input data.
|
||||
*/
|
||||
CRT(int cycles_per_line,
|
||||
int clocks_per_pixel_greatest_common_divisor,
|
||||
int height_of_display,
|
||||
Outputs::Display::ColourSpace colour_space,
|
||||
Outputs::Display::ColourSpace,
|
||||
int colour_cycle_numerator,
|
||||
int colour_cycle_denominator,
|
||||
int vertical_sync_half_lines,
|
||||
bool should_alternate,
|
||||
Outputs::Display::InputDataType data_type);
|
||||
Outputs::Display::InputDataType);
|
||||
|
||||
/*! Constructs a monitor-style CRT — one that will take only an RGB or monochrome signal, and therefore has
|
||||
no colour space or colour subcarrier frequency. This monitor will automatically map colour bursts to the black level.
|
||||
@@ -149,15 +93,15 @@ public:
|
||||
int clocks_per_pixel_greatest_common_divisor,
|
||||
int height_of_display,
|
||||
int vertical_sync_half_lines,
|
||||
Outputs::Display::InputDataType data_type);
|
||||
Outputs::Display::InputDataType);
|
||||
|
||||
/*! Exactly identical to calling the designated constructor with colour subcarrier information
|
||||
looked up by display type.
|
||||
*/
|
||||
CRT(int cycles_per_line,
|
||||
int minimum_cycles_per_pixel,
|
||||
Outputs::Display::Type display_type,
|
||||
Outputs::Display::InputDataType data_type);
|
||||
Outputs::Display::Type,
|
||||
Outputs::Display::InputDataType);
|
||||
|
||||
/*! Constructs a CRT with no guaranteed expectations as to input signal other than data type;
|
||||
this allows for callers that intend to rely on @c set_new_timing.
|
||||
@@ -169,7 +113,7 @@ public:
|
||||
void set_new_timing(
|
||||
int cycles_per_line,
|
||||
int height_of_display,
|
||||
Outputs::Display::ColourSpace colour_space,
|
||||
Outputs::Display::ColourSpace,
|
||||
int colour_cycle_numerator,
|
||||
int colour_cycle_denominator,
|
||||
int vertical_sync_half_lines,
|
||||
@@ -179,15 +123,15 @@ public:
|
||||
as though the new timing had been provided at construction. */
|
||||
void set_new_display_type(
|
||||
int cycles_per_line,
|
||||
Outputs::Display::Type display_type);
|
||||
Outputs::Display::Type);
|
||||
|
||||
/*! Changes the type of data being supplied as input.
|
||||
*/
|
||||
void set_new_data_type(Outputs::Display::InputDataType data_type);
|
||||
void set_new_data_type(Outputs::Display::InputDataType);
|
||||
|
||||
/*! Sets the CRT's intended aspect ratio — 4.0/3.0 by default.
|
||||
*/
|
||||
void set_aspect_ratio(float aspect_ratio);
|
||||
void set_aspect_ratio(float);
|
||||
|
||||
/*! Output at the sync level.
|
||||
|
||||
@@ -230,7 +174,7 @@ public:
|
||||
*/
|
||||
void output_data(int number_of_cycles, size_t number_of_samples);
|
||||
|
||||
/*! A shorthand form for output_data that assumes the number of cycles to output for is the same as the number of samples. */
|
||||
/*! A shorthand form for @c output_data that assumes the number of cycles to output for is the same as the number of samples. */
|
||||
void output_data(int number_of_cycles) {
|
||||
output_data(number_of_cycles, size_t(number_of_cycles));
|
||||
}
|
||||
@@ -245,7 +189,12 @@ public:
|
||||
@param amplitude The amplitude of the colour burst in 1/255ths of the amplitude of the
|
||||
positive portion of the wave.
|
||||
*/
|
||||
void output_colour_burst(int number_of_cycles, uint8_t phase, bool is_alternate_line = false, uint8_t amplitude = DefaultAmplitude);
|
||||
void output_colour_burst(
|
||||
int number_of_cycles,
|
||||
uint8_t phase,
|
||||
bool is_alternate_line = false,
|
||||
uint8_t amplitude = DefaultAmplitude
|
||||
);
|
||||
|
||||
/*! Outputs a colour burst exactly in phase with CRT expectations using the idiomatic amplitude.
|
||||
|
||||
@@ -284,7 +233,7 @@ public:
|
||||
}
|
||||
|
||||
/*! Sets the gamma exponent for the simulated screen. */
|
||||
void set_input_gamma(float gamma);
|
||||
void set_input_gamma(float);
|
||||
|
||||
enum CompositeSourceType {
|
||||
/// The composite function provides continuous output.
|
||||
@@ -307,7 +256,7 @@ public:
|
||||
void set_composite_function_type(CompositeSourceType type, float offset_of_first_sample = 0.0f);
|
||||
|
||||
/*! Nominates a section of the display to crop to for output. */
|
||||
void set_visible_area(Outputs::Display::Rect visible_area);
|
||||
void set_visible_area(Outputs::Display::Rect);
|
||||
|
||||
/*! @returns The rectangle describing a subset of the display, allowing for sync periods. */
|
||||
Outputs::Display::Rect get_rect_for_area(
|
||||
@@ -345,51 +294,78 @@ public:
|
||||
|
||||
/*! Sets the output brightness. */
|
||||
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 &, 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;
|
||||
// Incoming clock lengths are multiplied by @c time_multiplier_ to increase precision across the line.
|
||||
int time_multiplier_ = 1;
|
||||
|
||||
// Two flywheels regulate scanning; the vertical with a range much greater than the horizontal.
|
||||
Flywheel horizontal_flywheel_, vertical_flywheel_;
|
||||
|
||||
// A divider to reduce the vertcial flywheel into a 16-bit range.
|
||||
int vertical_flywheel_output_divider_ = 1;
|
||||
int cycles_since_horizontal_sync_ = 0;
|
||||
|
||||
/// Samples the flywheels to generate a raster endpoint, tagging it with the specified @c data_offset.
|
||||
Display::ScanTarget::Scan::EndPoint end_point(uint16_t data_offset);
|
||||
|
||||
struct Scan {
|
||||
enum Type {
|
||||
Sync, Level, Data, Blank, ColourBurst
|
||||
} type = Scan::Blank;
|
||||
int number_of_cycles = 0, number_of_samples = 0;
|
||||
uint8_t phase = 0, amplitude = 0;
|
||||
};
|
||||
std::array<FrameRecord, 4> frame_records_;
|
||||
size_t frame_record_pointer_ = 0;
|
||||
void output_scan(const Scan &scan);
|
||||
|
||||
uint8_t colour_burst_amplitude_ = 30;
|
||||
int colour_burst_phase_adjustment_ = 0xff;
|
||||
|
||||
int64_t phase_denominator_ = 1;
|
||||
int64_t phase_numerator_ = 0;
|
||||
int64_t colour_cycle_numerator_ = 1;
|
||||
bool is_alternate_line_ = false, phase_alternates_ = false, should_be_alternate_line_ = false;
|
||||
|
||||
void advance_cycles(
|
||||
int number_of_cycles,
|
||||
bool hsync_requested,
|
||||
bool vsync_requested,
|
||||
const Scan::Type,
|
||||
int number_of_samples);
|
||||
|
||||
Delegate *delegate_ = nullptr;
|
||||
int frames_since_last_delegate_call_ = 0;
|
||||
|
||||
// @c true exactly if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync).
|
||||
bool is_receiving_sync_ = false;
|
||||
|
||||
// @c true exactly if a sync level has triggered the suspicion that a vertical sync might be in progress.
|
||||
bool is_accumulating_sync_ = false;
|
||||
|
||||
// @c true once a vertical sync has been detected, until a prolonged period of non-sync has ended suspicion
|
||||
// of an ongoing vertical sync. Used to let horizontal sync free-run during vertical
|
||||
bool is_refusing_sync_ = false;
|
||||
|
||||
// Charges up during sync; depletes otherwise. Triggrs vertical sync upon hitting a required threshold.
|
||||
int sync_capacitor_charge_threshold_ = 0;
|
||||
|
||||
// Number of cycles since sync began, while sync lasts.
|
||||
int cycles_of_sync_ = 0;
|
||||
|
||||
// Number of cycles sync last ended. Used to defeat the prospect of vertical sync.
|
||||
int cycles_since_sync_ = 0;
|
||||
|
||||
int cycles_per_line_ = 1;
|
||||
|
||||
Outputs::Display::ScanTarget *scan_target_ = &Outputs::Display::NullScanTarget::singleton;
|
||||
Outputs::Display::ScanTarget::Modals scan_target_modals_;
|
||||
|
||||
// Based upon a black level to maximum excursion and positive burst peak of: NTSC: 882 & 143; PAL: 933 & 150.
|
||||
static constexpr uint8_t DefaultAmplitude = 41;
|
||||
|
||||
#ifndef NDEBUG
|
||||
size_t allocated_data_length_ = std::numeric_limits<size_t>::min();
|
||||
#endif
|
||||
};
|
||||
|
||||
}
|
||||
|
@@ -8,9 +8,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
namespace Outputs::CRT {
|
||||
|
||||
@@ -29,49 +31,59 @@ struct Flywheel {
|
||||
@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(int standard_period, int retrace_time, int sync_error_window) :
|
||||
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 {
|
||||
/// Indicates that no synchronisation events will occur in the queried window.
|
||||
None,
|
||||
/// Indicates that the next synchronisation event will be a transition into retrce.
|
||||
StartRetrace,
|
||||
/// Indicates that the next synchronisation event will be a transition out of retrace.
|
||||
EndRetrace
|
||||
};
|
||||
/*!
|
||||
Asks the flywheel for the first synchronisation event that will occur in a given time period,
|
||||
indicating whether a synchronisation request occurred at the start of the query window.
|
||||
|
||||
@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 number of cycles to look ahead.
|
||||
@param cycles_to_run_for The maximum number of cycles to look ahead.
|
||||
|
||||
@param cycles_advanced After this method has completed, contains the amount of time until
|
||||
the returned synchronisation event.
|
||||
|
||||
@returns The next synchronisation event.
|
||||
@returns A pair of the next synchronisation event and number of cycles until it occurs.
|
||||
*/
|
||||
inline SyncEvent get_next_event_in_period(bool sync_is_requested, int cycles_to_run_for, int *cycles_advanced) {
|
||||
std::pair<SyncEvent, int> 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 + 3*actual) >> 2;
|
||||
};
|
||||
|
||||
// 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_ = (3*expected_next_sync_ + time_now) >> 2;
|
||||
expected_next_sync_ = mix(expected_next_sync_, time_now);
|
||||
} else {
|
||||
++number_of_surprises_;
|
||||
|
||||
if(counter_ < retrace_time_ + (expected_next_sync_ >> 1)) {
|
||||
expected_next_sync_ = (3*expected_next_sync_ + standard_period_ + sync_error_window_) >> 2;
|
||||
expected_next_sync_ = mix(expected_next_sync_, standard_period_ + sync_error_window_);
|
||||
} else {
|
||||
expected_next_sync_ = (3*expected_next_sync_ + standard_period_ - sync_error_window_) >> 2;
|
||||
expected_next_sync_ = mix(expected_next_sync_, standard_period_ - sync_error_window_);
|
||||
}
|
||||
}
|
||||
last_adjustment_ = expected_next_sync_ - last_sync;
|
||||
@@ -82,18 +94,19 @@ struct Flywheel {
|
||||
|
||||
// End an ongoing retrace?
|
||||
if(counter_ < retrace_time_ && counter_ + proposed_sync_time >= retrace_time_) {
|
||||
proposed_sync_time = retrace_time_ - counter_;
|
||||
proposed_sync_time = require_positive(retrace_time_ - counter_);
|
||||
proposed_event = SyncEvent::EndRetrace;
|
||||
}
|
||||
|
||||
// Start a retrace?
|
||||
if(counter_ + proposed_sync_time >= expected_next_sync_) {
|
||||
proposed_sync_time = expected_next_sync_ - counter_;
|
||||
// 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;
|
||||
}
|
||||
|
||||
*cycles_advanced = proposed_sync_time;
|
||||
return proposed_event;
|
||||
return std::make_pair(proposed_event, proposed_sync_time);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -104,7 +117,7 @@ struct Flywheel {
|
||||
|
||||
@param event The synchronisation event to apply after that period.
|
||||
*/
|
||||
inline void apply_event(int cycles_advanced, SyncEvent event) {
|
||||
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_;
|
||||
@@ -127,7 +140,7 @@ struct Flywheel {
|
||||
|
||||
@returns The current output position.
|
||||
*/
|
||||
inline int get_current_output_position() const {
|
||||
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;
|
||||
@@ -139,60 +152,60 @@ struct Flywheel {
|
||||
|
||||
/*!
|
||||
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 get_locked_scan_period().
|
||||
the retrace period and it will then count up to @c locked_scan_period().
|
||||
|
||||
@returns The current output position.
|
||||
*/
|
||||
inline int get_current_phase() const {
|
||||
int current_phase() const {
|
||||
return counter_ - retrace_time_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns the amount of time since retrace last began. Time then counts monotonically up from zero.
|
||||
*/
|
||||
inline int get_current_time() const {
|
||||
int current_time() const {
|
||||
return counter_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns whether the output is currently retracing.
|
||||
*/
|
||||
inline bool is_in_retrace() const {
|
||||
bool is_in_retrace() const {
|
||||
return counter_ < retrace_time_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns the expected length of the scan period (excluding retrace).
|
||||
*/
|
||||
inline int get_scan_period() const {
|
||||
int scan_period() const {
|
||||
return standard_period_ - retrace_time_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns the actual length of the scan period (excluding retrace).
|
||||
*/
|
||||
inline int get_locked_scan_period() const {
|
||||
int locked_scan_period() const {
|
||||
return expected_next_sync_ - retrace_time_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns the expected length of a complete scan and retrace cycle.
|
||||
*/
|
||||
inline int get_standard_period() const {
|
||||
int standard_period() const {
|
||||
return standard_period_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns the actual current period for a complete scan (including retrace).
|
||||
*/
|
||||
inline int get_locked_period() const {
|
||||
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.
|
||||
*/
|
||||
inline int get_last_period_adjustment() const {
|
||||
int last_period_adjustment() const {
|
||||
return last_adjustment_;
|
||||
}
|
||||
|
||||
@@ -200,7 +213,7 @@ struct Flywheel {
|
||||
@returns the number of synchronisation events that have seemed surprising since the last time this method was called;
|
||||
a low number indicates good synchronisation.
|
||||
*/
|
||||
inline int get_and_reset_number_of_surprises() {
|
||||
int get_and_reset_number_of_surprises() {
|
||||
const int result = number_of_surprises_;
|
||||
number_of_surprises_ = 0;
|
||||
return result;
|
||||
@@ -209,37 +222,37 @@ struct Flywheel {
|
||||
/*!
|
||||
@returns A count of the number of retraces so far performed.
|
||||
*/
|
||||
inline int get_number_of_retraces() const {
|
||||
int number_of_retraces() const {
|
||||
return number_of_retraces_;
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns The amount of time this flywheel spends in retrace, as supplied at construction.
|
||||
*/
|
||||
inline int get_retrace_period() const {
|
||||
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.
|
||||
*/
|
||||
inline bool is_near_expected_sync() const {
|
||||
bool is_near_expected_sync() const {
|
||||
return
|
||||
(counter_ < (standard_period_ / 100)) ||
|
||||
(counter_ >= expected_next_sync_ - (standard_period_ / 100));
|
||||
}
|
||||
|
||||
private:
|
||||
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 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_; // 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_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; // A count of the surprising syncs.
|
||||
int number_of_retraces_ = 0; // A count of the number of retraces to date.
|
||||
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.
|
||||
|
||||
|
69
Outputs/CRT/MismatchWarner.hpp
Normal file
69
Outputs/CRT/MismatchWarner.hpp
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// MismatchWarner.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 06/10/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CRT.hpp"
|
||||
|
||||
namespace Outputs::CRT {
|
||||
|
||||
/*!
|
||||
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 &,
|
||||
const int number_of_frames,
|
||||
const int number_of_unexpected_vertical_syncs
|
||||
) final {
|
||||
auto &record = frame_records_[frame_record_pointer_ & (NumberOfFrameRecords - 1)];
|
||||
record.number_of_frames = number_of_frames;
|
||||
record.number_of_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs;
|
||||
++frame_record_pointer_;
|
||||
check_for_mismatch();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
frame_records_ = std::array<FrameRecord, NumberOfFrameRecords>{};
|
||||
}
|
||||
|
||||
private:
|
||||
Receiver &receiver_;
|
||||
struct FrameRecord {
|
||||
int number_of_frames = 0;
|
||||
int number_of_unexpected_vertical_syncs = 0;
|
||||
};
|
||||
|
||||
void check_for_mismatch() {
|
||||
if(frame_record_pointer_ * 2 >= NumberOfFrameRecords * 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr int NumberOfFrameRecords = 4;
|
||||
static_assert(!(NumberOfFrameRecords & (NumberOfFrameRecords - 1)));
|
||||
int frame_record_pointer_ = 0;
|
||||
std::array<FrameRecord, NumberOfFrameRecords> frame_records_;
|
||||
};
|
||||
|
||||
}
|
Reference in New Issue
Block a user