mirror of
https://github.com/TomHarte/CLK.git
synced 2026-04-22 08:16:42 +00:00
Start focussing on getting a good crop for 'static' machines.
This commit is contained in:
+111
-59
@@ -37,6 +37,8 @@ void CRT::set_new_timing(
|
||||
// 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;
|
||||
|
||||
@@ -86,6 +88,18 @@ void CRT::set_new_timing(
|
||||
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_);
|
||||
}
|
||||
|
||||
@@ -128,6 +142,8 @@ void CRT::set_composite_function_type(const CompositeSourceType type, const floa
|
||||
|
||||
// MARK: - Constructors.
|
||||
|
||||
CRT::CRT() : animation_curve_(Numeric::CubicCurve::easeInOut()) {}
|
||||
|
||||
CRT::CRT(
|
||||
const int cycles_per_line,
|
||||
const int clocks_per_pixel_greatest_common_divisor,
|
||||
@@ -137,7 +153,7 @@ CRT::CRT(
|
||||
const int vertical_sync_half_lines,
|
||||
const bool should_alternate,
|
||||
const Outputs::Display::InputDataType data_type
|
||||
) : animation_curve_(Numeric::CubicCurve::easeInOut()) {
|
||||
) : 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(
|
||||
@@ -156,7 +172,7 @@ CRT::CRT(
|
||||
const int clocks_per_pixel_greatest_common_divisor,
|
||||
const Outputs::Display::Type display_type,
|
||||
const Outputs::Display::InputDataType data_type
|
||||
) : animation_curve_(Numeric::CubicCurve::easeInOut()) {
|
||||
) : 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);
|
||||
@@ -168,7 +184,7 @@ CRT::CRT(
|
||||
const int height_of_display,
|
||||
const int vertical_sync_half_lines,
|
||||
const Outputs::Display::InputDataType data_type
|
||||
) : animation_curve_(Numeric::CubicCurve::easeInOut()) {
|
||||
) : 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(
|
||||
@@ -251,62 +267,8 @@ void CRT::advance_cycles(
|
||||
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) {
|
||||
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;
|
||||
};
|
||||
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_);
|
||||
};
|
||||
|
||||
// Zoom out very slightly if there's space; this avoids a cramped tight crop.
|
||||
if(
|
||||
active_rect_.size.width < 0.95 * scan_target_modals_.output_scale.x &&
|
||||
active_rect_.size.height < 0.95 * scan_target_modals_.output_scale.y
|
||||
) {
|
||||
active_rect_.scale(1.02f, 1.02f);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if(animation_step_ < AnimationSteps) {
|
||||
set_rect(current_rect());
|
||||
++animation_step_;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// }
|
||||
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;
|
||||
@@ -427,6 +389,96 @@ Outputs::Display::ScanTarget::Scan::EndPoint CRT::end_point(const uint16_t data_
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
+14
-3
@@ -47,11 +47,16 @@ static constexpr bool AlternatesPhase = false;
|
||||
}
|
||||
|
||||
class CRT;
|
||||
|
||||
struct Delegate {
|
||||
virtual void crt_did_end_batch_of_frames(CRT &, int frames, int unexpected_vertical_syncs) = 0;
|
||||
};
|
||||
|
||||
enum class Framing {
|
||||
AutomaticFixed,
|
||||
DynamicInRange,
|
||||
Static,
|
||||
};
|
||||
|
||||
/*! Models a class 2d analogue output device, accepting a serial stream of data including syncs
|
||||
and generating the proper set of output spans. Attempts to act and react exactly as a real
|
||||
TV would have to things like irregular or off-spec sync, and includes logic properly to track
|
||||
@@ -265,6 +270,8 @@ public:
|
||||
/*! Nominates a section of the display to crop to for output. */
|
||||
void set_visible_area(Outputs::Display::Rect);
|
||||
|
||||
void set_framing(Framing, Outputs::Display::Rect seed, Outputs::Display::Rect maximum);
|
||||
|
||||
/*! @returns The rectangle describing a subset of the display, allowing for sync periods. */
|
||||
Outputs::Display::Rect get_rect_for_area(
|
||||
int first_line_after_sync,
|
||||
@@ -303,8 +310,10 @@ public:
|
||||
void set_brightness(float);
|
||||
|
||||
private:
|
||||
CRT();
|
||||
|
||||
// Incoming clock lengths are multiplied by @c time_multiplier_ to increase precision across the line.
|
||||
int time_multiplier_ = 1;
|
||||
int time_multiplier_ = 0;
|
||||
|
||||
// Two flywheels regulate scanning; the vertical with a range much greater than the horizontal.
|
||||
Flywheel horizontal_flywheel_, vertical_flywheel_;
|
||||
@@ -376,7 +385,7 @@ private:
|
||||
bool frame_is_complete_ = false;
|
||||
|
||||
// Current state of cropping rectangle as communicated onwards.
|
||||
std::optional<Outputs::Display::Rect> posted_rect_;
|
||||
Outputs::Display::Rect posted_rect_;
|
||||
Outputs::Display::Rect previous_posted_rect_;
|
||||
Numeric::CubicCurve animation_curve_;
|
||||
|
||||
@@ -387,7 +396,9 @@ private:
|
||||
int level_changes_in_frame_ = 0;
|
||||
uint32_t last_level_ = 0;
|
||||
|
||||
Framing framing_ = Framing::AutomaticFixed;
|
||||
RectAccumulator rect_accumulator_;
|
||||
void posit(Display::Rect);
|
||||
|
||||
#ifndef NDEBUG
|
||||
size_t allocated_data_length_ = std::numeric_limits<size_t>::min();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
@@ -109,6 +110,10 @@ struct Flywheel {
|
||||
return std::make_pair(proposed_event, proposed_sync_time);
|
||||
}
|
||||
|
||||
bool was_stable() const {
|
||||
return std::abs(last_adjustment_) < 10; // TODO: don't hard code this nonsense.
|
||||
}
|
||||
|
||||
/*!
|
||||
Advances a nominated amount of time, applying a previously returned synchronisation event
|
||||
at the end of that period.
|
||||
|
||||
@@ -26,6 +26,11 @@ struct RectAccumulator {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Display::Rect> first_reading() const {
|
||||
if(candidates_.pushes() != 2) return std::nullopt;
|
||||
return candidates_.join(2);
|
||||
}
|
||||
|
||||
private:
|
||||
template <size_t n>
|
||||
struct RectHistory {
|
||||
@@ -36,10 +41,10 @@ private:
|
||||
if(stream_pointer_ == n) stream_pointer_ = 0;
|
||||
}
|
||||
|
||||
Display::Rect join() const {
|
||||
Display::Rect join(int limit = 0) const {
|
||||
return std::accumulate(
|
||||
stream_.begin() + 1,
|
||||
stream_.end(),
|
||||
limit > 0 ? stream_.begin() + limit : stream_.end(),
|
||||
stream_[0],
|
||||
[](const Display::Rect &lhs, const Display::Rect &rhs) {
|
||||
return lhs | rhs;
|
||||
|
||||
Reference in New Issue
Block a user