1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-10-08 03:17:17 +00:00

Compare commits

..

17 Commits

Author SHA1 Message Date
Thomas Harte
60b3c51085 Merge pull request #1598 from TomHarte/DynamicViewArea
Begin move towards automatic cropping.
2025-10-06 19:07:20 -04:00
Thomas Harte
d7b5a45417 Adopt even more aggressive mixing, avoid negative. 2025-10-06 16:20:54 -04:00
Thomas Harte
e11060bde8 Further improve asserting. 2025-10-06 16:16:06 -04:00
Thomas Harte
4653de9161 Pull out and comment on mix, improve asserts. 2025-10-06 16:11:59 -04:00
Thomas Harte
1926ad9215 Normalise and slightly reformat flywheel interface. 2025-10-06 14:53:08 -04:00
Thomas Harte
33d047c703 Add a const. 2025-10-06 14:38:40 -04:00
Thomas Harte
fadda00246 Eliminate flywheel 'get's, hence normalise CRT line lengths. 2025-10-06 14:36:39 -04:00
Thomas Harte
a3fed788d8 Reduce repetition. 2025-10-06 14:27:57 -04:00
Thomas Harte
dde31e8687 Reformat inner loop. 2025-10-06 14:26:03 -04:00
Thomas Harte
190fb009bc Clean up CRT.hpp for formatting. Switch pointer to reference. 2025-10-06 13:55:03 -04:00
Thomas Harte
62574d04c6 Avoid some redundant parameter names. 2025-10-06 13:32:28 -04:00
Thomas Harte
2496257bcf Adopt normative public-then-private ordering. 2025-10-06 13:28:04 -04:00
Thomas Harte
ab73b4de6b Split off the mismatch warner. 2025-10-06 13:27:10 -04:00
Thomas Harte
6c1c32baca Move flywheels local. 2025-10-04 22:42:56 -04:00
Thomas Harte
239cc15c8f Introduce cubic timing function. 2025-10-04 22:26:09 -04:00
Thomas Harte
6b437c3907 Merge pull request #1597 from TomHarte/NewShaker
Ensure CPCShakerTests is runnable.
2025-10-03 22:33:52 -04:00
Thomas Harte
4756f63169 Ensure CPCShakerTests is runnable. 2025-10-03 22:25:16 -04:00
11 changed files with 557 additions and 338 deletions

View File

@@ -13,6 +13,8 @@
#include "Machines/MachineTypes.hpp"
#include "Outputs/CRT/MismatchWarner.hpp"
#include "Analyser/Static/Atari2600/Target.hpp"
#include "Cartridges/Atari8k.hpp"

View File

@@ -9,6 +9,7 @@
#pragma once
#include "Outputs/CRT/CRT.hpp"
#include "Outputs/CRT/MismatchWarner.hpp"
#include "ClockReceiver/ClockReceiver.hpp"
#include <cstdint>

View File

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

View File

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

View File

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

View File

@@ -160,7 +160,7 @@ private:
@interface CPCShakerTests : XCTestCase
@end
@implementation CPCShakerTests {}
@implementation CPCShakerTests
- (void)testCSLPath:(NSString *)path name:(NSString *)name {
using namespace Storage::Automation;

View File

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

View File

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

View File

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

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