// // CRT.cpp // Clock Signal // // Created by Thomas Harte on 19/07/2015. // Copyright 2015 Thomas Harte. All rights reserved. // #include "CRT.hpp" #include #include #include #include using namespace Outputs::CRT; // MARK: - Input timing setup. 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." const bool is_first_set = time_multiplier_ == 0; // 63475 = 65535 * 31/32, i.e. the same 1/32 error as below is permitted. time_multiplier_ = 63487 / cycles_per_line; 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, // as a simple way of avoiding not-quite-exact comparison issues while still being true enough to // the gist for simple debugging. sync_capacitor_charge_threshold_ = ((vertical_sync_half_lines - 2) * cycles_per_line) >> 1; // 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. 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. 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_.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_.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; scan_target_modals_.colour_cycle_numerator = colour_cycle_numerator; scan_target_modals_.colour_cycle_denominator = colour_cycle_denominator; // Default crop: middle 90%. if(is_first_set) { posted_rect_ = Display::Rect( 0.05f * scan_target_modals_.output_scale.x, 0.05f * scan_target_modals_.output_scale.y, 0.9f * scan_target_modals_.output_scale.x, 0.9f * scan_target_modals_.output_scale.y ); scan_target_modals_.visible_area = posted_rect_; } scan_target_->set_modals(scan_target_modals_); } 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: scan_target_modals_.intended_gamma = 2.8f; set_new_timing( cycles_per_line, (displayType == Outputs::Display::Type::PAL50) ? 312 : 262, PAL::ColourSpace, PAL::ColourCycleNumerator, PAL::ColourCycleDenominator, PAL::VerticalSyncLength, PAL::AlternatesPhase); break; case Outputs::Display::Type::NTSC60: scan_target_modals_.intended_gamma = 2.2f; set_new_timing( cycles_per_line, 262, NTSC::ColourSpace, NTSC::ColourCycleNumerator, NTSC::ColourCycleDenominator, NTSC::VerticalSyncLength, NTSC::AlternatesPhase); break; } } 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 { colour_burst_phase_adjustment_ = 0xff; } } // MARK: - Constructors. CRT::CRT() : animation_curve_(Numeric::CubicCurve::easeInOut()) {} 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 ) : CRT() { 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 ); } 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 ) : CRT() { 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( 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 ) : CRT() { 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 ); } // Use some from-thin-air arbitrary constants for default timing, otherwise passing // construction off to one of the other constructors. CRT::CRT(const Outputs::Display::InputDataType data_type) : CRT(100, 1, 100, 1, data_type) {} // MARK: - Sync loop 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 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. 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 = 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(); Outputs::Display::ScanTarget::Scan *const next_scan = is_output_segment ? scan_target_->begin_scan() : nullptr; frame_is_complete_ &= !is_output_segment || bool(next_scan); did_output |= is_output_segment; // If outputting, store the start location and scan constants. if(next_scan) { next_scan->end_points[0] = end_point(); next_scan->composite_amplitude = colour_burst_amplitude_; } // 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 == horizontal_event.second ? horizontal_event.first : Flywheel::SyncEvent::None ); const auto active_vertical_event = next_run_length == vertical_event.second ? vertical_event.first : Flywheel::SyncEvent::None; vertical_flywheel_.apply_event(next_run_length, active_vertical_event); if(active_vertical_event == Flywheel::SyncEvent::StartRetrace) { if(frame_is_complete_ && captures_in_rect_ > 5 && vertical_flywheel_.was_stable()) { posit(active_rect_); } active_rect_ = Display::Rect(65536.0f, 65536.0f, 0.0f, 0.0f); frame_is_complete_ = true; captures_in_rect_ = 0; level_changes_in_frame_ = 0; } // End the scan if necessary. if(next_scan) { next_scan->end_points[1] = end_point(); if(frame_is_complete_ && (levels_are_interesting_ || number_of_samples > 1)) { // Omit any scan that's not within the central 95% of the display. // const uint16_t hard_left = scan_target_modals_.output_scale.x * 0.025f; // const uint16_t hard_right = scan_target_modals_.output_scale.x * 0.975f; // const uint16_t hard_top = scan_target_modals_.output_scale.y * 0.025f; // const uint16_t hard_bottom = scan_target_modals_.output_scale.x * 0.975f; // if( // (next_scan->end_points[0].y >= hard_top || next_scan->end_points[1].y >= hard_top) && // (next_scan->end_points[0].y <= hard_bottom || next_scan->end_points[1].y <= hard_bottom) && // (next_scan->end_points[0].x >= hard_left || next_scan->end_points[1].y >= hard_left) && // (next_scan->end_points[0].x <= hard_right || next_scan->end_points[1].y <= hard_right) // ) { ++captures_in_rect_; active_rect_.expand( next_scan->end_points[0].x, next_scan->end_points[1].x, next_scan->end_points[0].y, next_scan->end_points[1].y ); // } } scan_target_->end_scan(); } 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(horizontal_event.first == Flywheel::SyncEvent::EndRetrace) { cycles_since_horizontal_sync_ = 0; // This is unnecessary, strictly speaking, but seeks to help ScanTargets fit as // much as possible into a fixed range. phase_numerator_ %= phase_denominator_; if(!phase_numerator_) phase_numerator_ += phase_denominator_; } // Announce event. const auto event = 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(), colour_burst_amplitude_); // If retrace is starting, update phase if required and mark no colour burst spotted yet. if(horizontal_event.first == Flywheel::SyncEvent::StartRetrace) { should_be_alternate_line_ ^= phase_alternates_; colour_burst_amplitude_ = 0; } } // Announce vertical sync events. if(next_run_length == vertical_event.second && vertical_event.first != Flywheel::SyncEvent::None) { const auto event = 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(), colour_burst_amplitude_); } // 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_; 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() ); frames_since_last_delegate_call_ = 0; } } } } if(did_output) { scan_target_->submit(); } } 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); 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_), }; } void CRT::posit(Display::Rect rect) { // Static framing: don't evaluate. if(framing_ == Framing::Static) { return; } // Zoom out very slightly if there's space; this avoids a cramped tight crop. if( rect.size.width < 0.95 * scan_target_modals_.output_scale.x && rect.size.height < 0.95 * scan_target_modals_.output_scale.y ) { rect.scale(1.02f, 1.02f); } // Scale and push a rect. const auto set_rect = [&](const Display::Rect &rect) { scan_target_modals_.visible_area = rect; scan_target_modals_.visible_area.origin.x /= scan_target_modals_.output_scale.x; scan_target_modals_.visible_area.size.width /= scan_target_modals_.output_scale.x; scan_target_modals_.visible_area.origin.y /= scan_target_modals_.output_scale.y; scan_target_modals_.visible_area.size.height /= scan_target_modals_.output_scale.y; scan_target_->set_modals(scan_target_modals_); }; // Get current interpolation between previous_posted_rect_ and posted_rect_. const auto current_rect = [&] { const auto animation_time = animation_curve_.value(float(animation_step_) / float(AnimationSteps)); return previous_posted_rect_ * (1.0f - animation_time) + posted_rect_ * animation_time; }; // Continue with any ongoing animation. if(animation_step_ < AnimationSteps) { set_rect(current_rect()); ++animation_step_; if(animation_step_ == AnimationSteps) { if(framing_ == Framing::AutomaticFixed) { framing_ = Framing::Static; return; } } } // TODO: possibly start with this approach in dynamic land, too? if(framing_ == Framing::AutomaticFixed) { rect_accumulator_.posit(rect); if(const auto reading = rect_accumulator_.first_reading(); reading.has_value()) { previous_posted_rect_ = posted_rect_; posted_rect_ = *reading; animation_step_ = 0; return; } } // If here: apply dynamic logic. // Limit visibility to the central 90% of the display regardless. /* const auto Middle95 = Display::Rect( 0.05f * scan_target_modals_.output_scale.x, 0.075f * scan_target_modals_.output_scale.y, 0.90f * scan_target_modals_.output_scale.x, 0.90f * scan_target_modals_.output_scale.y); const auto output_frame = rect_accumulator_.posit(active_rect_ & Middle95); // if(!posted_rect_.has_value()) { // // Startup condition; accept any reasonable frame if the accumulator is just getting started. // const auto any_frame = rect_accumulator_.any_union(); // if(any_frame) { // posted_rect_ = any_frame; // set_rect(*posted_rect_); // } // } else { if(output_frame && (!posted_rect_ || *output_frame != *posted_rect_)) { previous_posted_rect_ = posted_rect_ ? current_rect() : *output_frame; posted_rect_ = *output_frame; animation_step_ = 0; } // TODO: probably something more gradated. // E.g. if there is at least one level colour change, zoom out a little bit. // Otherwise zoom in somewhere closer. levels_are_interesting_ = level_changes_in_frame_ >= 5; // } */ } // 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_.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; if(colour_burst_phase_adjustment_ != 0xff) phase_numerator_ = (phase_numerator_ & ~63) + colour_burst_phase_adjustment_; // Multiply the phase_numerator_ up to be to the proper scale. phase_numerator_ = (phase_numerator_ * phase_denominator_) >> 8; // Crib the colour burst 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; 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(); if(this_is_sync) { // If this is sync then either begin or continue a sync accumulation phase. is_accumulating_sync_ = true; cycles_since_sync_ = 0; } 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; if(cycles_since_sync_ > (cycles_per_line_ >> 2)) { cycles_of_sync_ = 0; is_accumulating_sync_ = false; is_refusing_sync_ = false; } } 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; 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); hsync_requested = false; number_of_cycles = overshoot; } is_refusing_sync_ = true; vsync_requested = true; } } advance_cycles(number_of_cycles, hsync_requested, vsync_requested, scan.type, scan.number_of_samples); } /* These all merely channel into advance_cycles, supplying appropriate arguments */ void CRT::output_sync(const int number_of_cycles) { output_scan(Scan{ .type = Scan::Type::Sync, .number_of_cycles = number_of_cycles, }); } void CRT::output_blank(const int number_of_cycles) { output_scan(Scan{ .type = Scan::Type::Blank, .number_of_cycles = number_of_cycles, }); } void CRT::output_level(const int number_of_cycles) { scan_target_->end_data(1); output_scan(Scan{ .type = Scan::Type::Level, .number_of_cycles = number_of_cycles, .number_of_samples = 1, }); } void CRT::output_colour_burst( const int number_of_cycles, const uint8_t phase, const bool is_alternate_line, const uint8_t amplitude ) { is_alternate_line_ = is_alternate_line; output_scan(Scan{ .type = Scan::Type::ColourBurst, .number_of_cycles = number_of_cycles, .phase = phase, .amplitude = uint8_t(amplitude >> 1), }); } void CRT::output_default_colour_burst(const int number_of_cycles, const 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 ); } void CRT::set_immediate_default_phase(const float phase) { phase_numerator_ = int(std::fmod(phase, 1.0f) * float(phase_denominator_)); } void CRT::output_data(const int number_of_cycles, const size_t number_of_samples) { #ifndef NDEBUG // assert(number_of_samples > 0); // assert(number_of_samples <= allocated_data_length_); // allocated_data_length_ = std::numeric_limits::min(); #endif scan_target_->end_data(number_of_samples); output_scan(Scan{ .type = Scan::Type::Data, .number_of_cycles = number_of_cycles, .number_of_samples = int(number_of_samples), }); } // MARK: - Getters. Outputs::Display::Rect CRT::get_rect_for_area( [[maybe_unused]] int first_line_after_sync, [[maybe_unused]] int number_of_lines, [[maybe_unused]] int first_cycle_after_sync, [[maybe_unused]] int number_of_cycles, [[maybe_unused]] const float aspect_ratio ) const { return Outputs::Display::Rect(); // assert(number_of_cycles > 0); // assert(number_of_lines > 0); // assert(first_line_after_sync >= 0); // assert(first_cycle_after_sync >= 0); // // // Scale up x coordinates and add a little extra leeway to y. // first_cycle_after_sync *= time_multiplier_; // number_of_cycles *= time_multiplier_; // // first_line_after_sync -= 2; // number_of_lines += 4; // // // Determine prima facie x extent. // 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. // first_cycle_after_sync = std::max(horizontal_retrace_period, first_cycle_after_sync); // number_of_cycles = std::min(horizontal_period - first_cycle_after_sync, number_of_cycles); // // float start_x = float(first_cycle_after_sync - horizontal_retrace_period) / float(horizontal_scan_period); // float width = float(number_of_cycles) / float(horizontal_scan_period); // // // Determine prima facie y extent. // 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. // first_line_after_sync = std::max( // first_line_after_sync * horizontal_period, // vertical_retrace_period // ) / horizontal_period; // number_of_lines = std::min( // vertical_period - first_line_after_sync * horizontal_period, // number_of_lines * horizontal_period // ) / horizontal_period; // // float start_y = // float(first_line_after_sync * horizontal_period - vertical_retrace_period) / // float(vertical_scan_period); // float height = float(number_of_lines * horizontal_period) / vertical_scan_period; // // // Pick a zoom that includes the entire requested visible area given the aspect ratio constraints. // const float adjusted_aspect_ratio = (3.0f*aspect_ratio / 4.0f); // const float ideal_width = height * adjusted_aspect_ratio; // if(ideal_width > width) { // start_x -= (ideal_width - width) * 0.5f; // width = ideal_width; // } else { // float ideal_height = width / adjusted_aspect_ratio; // start_y -= (ideal_height - height) * 0.5f; // height = ideal_height; // } // // // TODO: apply absolute clipping constraints now. // // return Outputs::Display::Rect(start_x, start_y, width, height); } Outputs::Display::ScanStatus CRT::get_scaled_scan_status() const { 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([[maybe_unused]] 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_); }